Hmbown/CodeWhale · error · ExecError
background preview capture failed
Error message
background preview capture failed: ${r.stderr} What it means
updatePreview captures a window-scoped screenshot of the bound app's window with macOS `screencapture -x -o -l <window_id>`, writing to a temp PNG that is renamed into the preview path. If screencapture exits non-zero, this error surfaces its stderr. Capture is a preview-only step; its failure means no background screenshot was produced.
Solutions
- Grant Screen Recording permission (System Settings → Privacy & Security → Screen Recording) to the app running the backend, then restart it.
- Re-run open_application/observe so window_info returns a fresh, live window_id before capturing.
- Test the capture manually: `screencapture -x -o -l <window_id> /tmp/t.png` to see the raw error.
- If stderr shows a stale window id after app restart, rebind the application (pid/window changed).
Example fix
// before await updatePreview(); // background preview capture failed: could not create image from window id // after: rebind to refresh the window id, ensure screen-recording permission await open_application(app.ref); await observe(); // triggers a fresh capture
Defensive patterns
Strategy: try-catch
Validate before calling
const win = await native('window_info', { app_ref: appRef });
if (!win?.window_id) throw new Error('no live window to capture — re-observe first'); Try / catch
try {
await updatePreview();
} catch (e) {
if (String(e.message).startsWith('background preview capture failed')) {
console.error('Check Screen Recording permission and that the window is still open:', e.message);
await open_application(appRef); // refresh window_id, then retry once
} else throw e;
} Prevention
- Grant Screen Recording permission to the host app before starting automation and after macOS upgrades.
- Refresh window_info before each capture; never reuse window ids across rebinding.
- Smoke-test `screencapture -x -o -l <id> /tmp/t.png` in environment preflight.
- Treat preview failure as non-fatal where the workflow can continue on accessibility data alone.
When it happens
Trigger: Calling the background preview/observe flow where `screencapture` fails: invalid or closed window_id from native("window_info"), missing Screen Recording permission for the host process, or a screencapture timeout (8s) / tool error reported via stderr.
Common situations: First-run macOS privacy prompt for Screen Recording denied or never granted; target window closed between window_info and the capture; running in an SSH/headless session without GUI access; macOS update resetting TCC permissions for the terminal/app.
Related errors
- screencapture exited
- open_application first to choose the preview app
- shared_pointer_required
- agent action=claim widens an enforced write scope, and the…
- agent profile may not disable approval_required
AI-assisted analysis of Hmbown/CodeWhale@73e0f67d83 (2026-09-22).
Data as JSON: /api/errors/69111edbdb2ff34a.
Report an issue: GitHub.
Appendix: source
Thrown at crates/tui/plugins/computer-use/src/backends/darwin.mjs:333
if (tool === "pointer_sequence") requireSharedPointer();
if (!exec.runInputLease) throw new ExecError("This executor cannot safely own held input; update Computer Use");
if ((await native("input_capabilities"))?.input_lease !== 1) throw new ExecError("The native helper needs an update for disconnect-safe held input");
const helper = await nativeHelper();
try {
return await exec.runInputLease(helper, [JSON.stringify({ tool, args: { ...args, ...yieldArgs, input_app_ref: state.inputApp, foreground_input: state.foregroundInput, owner_pipe: true, input_lease: true } })]);
} catch (error) {
error.code = nativeErrorCode(error.message) ?? error.code;
throw error;
}
}
async function updatePreview(show = false) {
const win = await native("window_info", { app_ref: state.inputApp });
const dir = path.join(stateDir(), "preview");
fs.mkdirSync(dir, { recursive: true, mode: 0o700 });
const temp = path.join(dir, "next.png"), file = path.join(dir, "latest.png");
const r = await runL("screencapture", ["-x", "-o", "-l", String(win.window_id), "-t", "png", temp], { timeoutMs: 8000 });
if (r.code !== 0) throw new ExecError(`background preview capture failed: ${r.stderr}`);
fs.renameSync(temp, file);
const p = state.pointer;
// The user's own hardware cursor goes on the preview too, so the panel
// shows both pointers in the same window-relative space.
let userCursor = null;
try { userCursor = await native("cursor_position"); } catch {}
await native("preview_notify", { enabled: true, show, title: `Codewhale · ${win.name} · ${state.foregroundInput ? "Shared desktop control" : "Background app control"}`, x: p ? (p.x-win.points.x)/win.points.w : -1, y: p ? (p.y-win.points.y)/win.points.h : -1,
user_x: userCursor && Number.isFinite(userCursor.x) ? (userCursor.x-win.points.x)/win.points.w : -1,
user_y: userCursor && Number.isFinite(userCursor.y) ? (userCursor.y-win.points.y)/win.points.h : -1 });
// Any successful capture (bind, explicit preview, action refresh) starts
// the live refresh; the tick itself re-enters this function as a no-op.
if (state.previewEnabled && state.inputApp) startPreviewLoop();
return { enabled: true, file, app: state.inputApp, pointer: p };
}
// ---------- pointer input ----------
// Our qualified raw pointer path uses the shared event tap, which moves
// the user's real cursor. Process/window-directed mouse delivery has notView on GitHub (pinned to 73e0f67d83)