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

  1. Read the stderr tail in the message: it names the failing tool and reason; fix that tool's environment first (DISPLAY/WAYLAND_DISPLAY/XAUTHORITY).
  2. Verify the screenshot binary works standalone with the same args (e.g. run `import -window root /tmp/x.png`).
  3. Under Wayland, install/switch to a Wayland-capable tool (grim, gnome-screenshot, spectacle) or use the XWayland/XDG portal path.
  4. If a crop region was passed, validate x/y/w/h are non-negative and within the virtual screen bounds before calling.
  5. 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

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


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)