Hmbown/CodeWhale · error · ExecError
screenshot path must end in .png, .jpg or .jpeg
Error message
screenshot path must end in .png, .jpg or .jpeg
What it means
The screenshot API validates that the requested output path has a .png, .jpg or .jpeg extension (case-insensitive). The extension also selects the capture format via the underlying screencapture tool, so an unknown extension cannot be mapped to a format.
Solutions
- End the path in .jpg/.jpeg (preferred: much smaller for photographic screen content) or .png for lossless
- Convert the captured file to another format afterwards with a separate tool if another format is needed
- Omit outPath entirely to get an auto-generated timestamped .jpg path
- Fix filename templates that append unsupported suffixes
Example fix
// before
await screenshot({ outPath: "shot.bmp" });
// after
await screenshot({ outPath: "shot.jpg" }); Defensive patterns
Strategy: validation
Validate before calling
if (!/\.(png|jpe?g)$/i.test(outPath)) throw new Error(`unsupported screenshot format: ${outPath}; use .png, .jpg or .jpeg`); Try / catch
try {
await screenshot({ outPath });
} catch (e) {
if (e instanceof ExecError && e.message.includes("must end in .png")) {
await screenshot({ outPath: outPath.replace(/\.\w+$/, "") + ".jpg" });
} else throw e;
} Prevention
- Prefer .jpg for screenshots (much smaller); reserve .png for pixel-exact comparisons
- Validate extensions in one shared filename helper
- Let the backend generate its timestamped path when the name does not matter
When it happens
Trigger: Calling screenshot with outPath like 'shot.bmp', 'shot.txt', 'shot' (no extension), 'shot.tiff', or a path with a trailing character after the extension.
Common situations: Caller picking an image format the backend does not support (bmp/webp); building filenames from templates that append suffixes; forgetting the extension entirely.
Understand the failure class
Background: "Must be a positive integer", "Invalid value", "Unsupported": the invalid-argument-value error family, when a library rejects the value you pass — this error's family across 35 libraries.
Related errors
- choose app_ref or region, not both
- display index must be a positive integer
- InvalidInput
- region must be integer [x,y,width,height] with positive size
- region must be [x, y, w, h] in screen points
AI-assisted analysis of Hmbown/CodeWhale@73e0f67d83 (2026-09-22).
Data as JSON: /api/errors/cbbe33de55357574.
Report an issue: GitHub.
Appendix: source
Thrown at crates/tui/plugins/computer-use/src/backends/darwin.mjs:519
// ---------- screenshots ----------
function recordingsDir() {
return process.env.CODEWHALE_CU_RECORDINGS_DIR || path.join(os.homedir(), ".codewhale-cu", "recordings");
}
async function screenshot({ display, region, app_ref, window_id, path: outPath } = {}) {
// Once an app is selected, ordinary observations follow it behind the
// user's work. An explicit display/region remains a deliberate desktop capture.
if (app_ref === undefined && display === undefined && region === undefined) app_ref = state.inputApp ?? undefined;
const dir = recordingsDir();
fs.mkdirSync(dir, { recursive: true });
// JPEG, not PNG. A screen is photographic content — gradients, wallpaper,
// antialiased text — and lossless compression of it is enormous: the same
// 5760x3240 frame is 21.8MB as PNG and 2.1MB as JPEG, at full resolution
// and with terminal text still crisp. PNG stays available by asking for a
// `.png` path, which is what a pixel-exact comparison wants.
const file = outPath || path.join(dir, `shot-${new Date().toISOString().replace(/[:.]/g, "-")}-${crypto.randomBytes(3).toString("hex")}.jpg`);
if (!/\.(png|jpe?g)$/i.test(file)) throw new ExecError("screenshot path must end in .png, .jpg or .jpeg");
const args = ["-x", "-t", /\.png$/i.test(file) ? "png" : "jpg"];
const disp = display ?? state.activeDisplay;
// An explicit app reference resolves first and alone: nothing may run
// before it and redirect the capture to another target.
const window = app_ref !== undefined ? await native("window_info", { app_ref, window_id }) : null;
if (window && region) throw new ExecError("choose app_ref or region, not both");
// On the display path, resolve displays before capturing so an unknown
// index is a clean error instead of a raster silently labelled with another
// display's geometry — list_displays reports `index` and `id` separately,
// and a caller passing the id would otherwise get points and scale that
// mis-target every later coordinate. A window capture ignores `display`.
let displays = null;
if (!window) {
displays = await displayInfo();
if (disp != null && disp !== "all" && !displays.some((x) => x.index === disp)) {
throw new ExecError(`no display ${disp}; have [${displays.map((x) => x.index).join(", ")}] — screenshot takes the display index from list_displays, not its id`);
}
}View on GitHub (pinned to 73e0f67d83)