Hmbown/CodeWhale · error · ExecError
exited
Error message
${cmd} exited ${r.code}: ${r.stderr.trim().slice(0, 300)} What it means
This ExecError is thrown by the Linux screenshot helper when an external screenshot command (e.g. import, scrot, gnome-screenshot, or ImageMagick convert for cropping) exits with a non-zero status. The library wraps the command's exit code and the first 300 bytes of stderr so the developer can see exactly why the external tool failed. It is a deliberate failure surfaced from the subprocess, not an internal bug.
Solutions
- Read the stderr tail in the message: it names the failing tool and reason; fix that tool's environment first (DISPLAY/WAYLAND_DISPLAY/XAUTHORITY).
- Verify the screenshot binary works standalone with the same args (e.g. run `import -window root /tmp/x.png`).
- Under Wayland, install/switch to a Wayland-capable tool (grim, gnome-screenshot, spectacle) or use the XWayland/XDG portal path.
- If a crop region was passed, validate x/y/w/h are non-negative and within the virtual screen bounds before calling.
- Check disk space and write permissions for the temp screenshot file location.
Example fix
// before
await takeShot({ region: [-50, -20, 800, 600] });
// after
const region = [Math.max(0, x), Math.max(0, y), w, h];
await takeShot({ region }); Defensive patterns
Strategy: fallback
Validate before calling
import { execFileSync } from 'child_process';
function canScreenshot(cmd) {
try { execFileSync('sh', ['-c', `command -v ${cmd}`], { stdio: 'ignore' }); return true; } catch { return false; }
}
const regionOk = (r) => !r || (r.every(Number.isFinite) && r[0] >= 0 && r[1] >= 0 && r[2] > 0 && r[3] > 0); Type guard
const isValidRegion = (r) => Array.isArray(r) && r.length === 4 && r.every((n) => Number.isFinite(n) && n >= 0);
Try / catch
try {
const shot = await takeShot({ region });
} catch (e) {
if (e instanceof ExecError && /import|scrot|convert/.test(e.message)) {
// switch to a Wayland-capable tool or fix DISPLAY and retry once
const shot = await takeShot({});
} else throw e;
} Prevention
- Install a session-appropriate screenshot tool (grim on Wayland, scrot/import on X11) and smoke-test it with the same args.
- Keep DISPLAY/XAUTHORITY correct when connecting over SSH or from containers.
- Validate crop regions: finite, non-negative, within screen bounds.
- Check free disk space where temp screenshots are written.
- Prefer running the plugin inside the desktop session's user environment.
When it happens
Trigger: Calling takeShot (public screenshot action) when the resolved screenshot command fails: tool missing from PATH is caught earlier by `need`, but a present-yet-broken tool (no DISPLAY, Wayland without portal support, invalid -crop region geometry, unwritable output file) exits non-zero and triggers this throw.
Common situations: Running under Wayland with an X11-only tool like `import`; missing X authority in headless/SSH sessions; crop region coordinates outside the screen or negative after Math.round; disk full or HOME not writable so the temp file cannot be created.
Related errors
- AT-SPI action failed
- linux backend needs " " for — install it and retry
- no screenshot taken yet on this computer — call screenshot…
- wtype failed
- application not found or name is ambiguous in the AT-SPI…
AI-assisted analysis of Hmbown/CodeWhale@73e0f67d83 (2026-09-22).
Data as JSON: /api/errors/0fd1c878fa12ce2a.
Report an issue: GitHub.
Appendix: source
Thrown at crates/tui/plugins/computer-use/src/backends/linux.mjs:143
}
/** Capture a PNG to `file`, optionally cropped to region [x,y,w,h] points. */
async function takeShot(file, region) {
outputPath(file);
const { cmd, base } = await shotTool();
let args = [...base];
if (cmd === "grim") {
if (region) args.push("-g", `${Math.round(region[0])},${Math.round(region[1])} ${Math.round(region[2])}x${Math.round(region[3])}`);
args.push(file);
} else if (cmd === "scrot") {
if (region) args.push("-a", `${Math.round(region[0])},${Math.round(region[1])},${Math.round(region[2])},${Math.round(region[3])}`);
args.push(file);
} else {
if (region) args.push("-crop", `${Math.round(region[2])}x${Math.round(region[3])}+${Math.round(region[0])}+${Math.round(region[1])}`);
args.push(file);
}
const r = await run(cmd, args, { timeoutMs: 10_000 });
if (r.code !== 0) throw new ExecError(`${cmd} exited ${r.code}: ${r.stderr.trim().slice(0, 300)}`, r);
}
function recordingsDir() {
return path.resolve(process.env.CODEWHALE_CU_RECORDINGS_DIR || path.join(os.homedir(), ".codewhale-cu", "recordings"));
}
async function xdotool(args, opts = {}) {
need("xdotool", "input on X11");
throwIfAborted();
const r = await run("xdotool", args, opts);
throwIfAborted();
if (r.code !== 0) throw new ExecError(`xdotool ${args[0]} exited ${r.code}: ${r.stderr.trim().slice(0, 200)}`, r);
return r.stdout.trim();
}
async function ydotool(args, opts = {}) {
need("ydotool", "input on Wayland (ydotool needs its daemon running: sudo ydotoold)");
throwIfAborted();View on GitHub (pinned to 73e0f67d83)