garrytan/gstack · error · Error
Cannot resolve real path: ${screenshotPath} (${err.code})
Error message
Cannot resolve real path: ${screenshotPath} (${err.code}) What it means
Thrown by the annotated-screenshot path validator in snapshot.ts. realpathSync() failed on the screenshot output path with an error code other than ENOENT (which is handled separately). The message echoes the failing path and the fs error code (e.g. EACCES, ELOOP, ENOTDIR). It guards the screenshot writer against unreadable, looping, or non-directory segments so the path can never be dereferenced blindly.
Source
Thrown at browse/src/snapshot.ts:380
const nodeFs = require('fs') as typeof import('fs');
const absolute = nodePath.resolve(screenshotPath);
const safeDirs = [TEMP_DIR, process.cwd()].map((d: string) => {
try { return nodeFs.realpathSync(d); } catch (err: any) { if (err?.code !== 'ENOENT') throw err; return d; }
});
let realPath: string;
try {
realPath = nodeFs.realpathSync(absolute);
} catch (err: any) {
if (err.code === 'ENOENT') {
try {
const dir = nodeFs.realpathSync(nodePath.dirname(absolute));
realPath = nodePath.join(dir, nodePath.basename(absolute));
} catch (err2: any) {
if (err2?.code !== 'ENOENT') throw err2;
realPath = absolute;
}
} else {
throw new Error(`Cannot resolve real path: ${screenshotPath} (${err.code})`);
}
}
if (!safeDirs.some((dir: string) => isPathWithin(realPath, dir))) {
throw new Error(`Path must be within: ${safeDirs.join(', ')}`);
}
}
try {
// Inject overlay divs at each ref's bounding box
const boxes: Array<{ ref: string; box: { x: number; y: number; width: number; height: number } }> = [];
for (const [ref, entry] of refMap) {
try {
const box = await entry.locator.boundingBox({ timeout: 1000 });
if (box) {
boxes.push({ ref: `@${ref}`, box });
}
} catch (err: any) {
// Element may be offscreen, hidden, or page navigated — skip
if (!err?.message?.includes('Timeout') && !err?.message?.includes('timeout') && !err?.message?.includes('closed') && !err?.message?.includes('Target') && !err?.message?.includes('Execution context')) throw err;View on GitHub (pinned to 94993f7401)
Solutions
- Re-run the same command with --output-path pointing at a directory you own (e.g. inside the current working directory or the system temp dir).
- Inspect the error code in parentheses: EACCES → fix permissions/ownership of every parent dir; ELOOP → remove the offending symlink loop; ENOTDIR → make sure every prefix segment except the final filename is a directory.
- Drop --output-path entirely so snapshot.ts falls back to `${TEMP_DIR}/browse-annotated.png`, which is always writable.
- If running under systemd/launchd/container, confirm the service user has read+resolve rights on the full realpath chain, not just the leaf.
Example fix
// before
snapshot(page, { annotate: true, outputPath: '/root/protected/shot.png' });
// after
snapshot(page, { annotate: true }); // writes to TEMP_DIR/browse-annotated.png Defensive patterns
Strategy: validation
Validate before calling
const fs = require('fs');
const path = require('path');
function assertScreenshotPathWritable(p: string, safeDirs: string[]): void {
const abs = path.resolve(p);
// Walk the path and verify each prefix is a directory we can traverse
const dirs = abs.split(path.sep).reduce<string[]>((acc, seg, i) => {
if (i === 0) return [seg || '/'];
return [...acc, path.join(acc[acc.length - 1], seg)];
}, []).slice(0, -1); // exclude the file itself
for (const d of dirs) {
try { const st = fs.statSync(d); if (!st.isDirectory()) throw new Error(`ENOTDIR: ${d}`); }
catch (e: any) { throw new Error(`Cannot resolve real path: ${p} (parent ${d} unreachable: ${e.code ?? e.message})`); }
}
} Try / catch
try {
await snapshot(session, { annotate: true, outputPath });
} catch (e: any) {
if (/^Cannot resolve real path:/.test(e.message)) {
// fall back to the default temp location and retry
outputPath = `${require('os').tmpdir()}/browse-annotated.png`;
await snapshot(session, { annotate: true, outputPath });
} else throw e;
} Prevention
- Always pass an --output-path inside process.cwd() or the system temp dir.
- Run `readlink -f` on the output path before invoking snapshot to catch symlink loops and permission walls early.
- In CI, run the browse process as the same user that owns the output directory.
- Default outputPath to undefined and let snapshot.ts pick the temp location.
When it happens
Trigger: Calling snapshot with opts.annotate=true and an opts.outputPath whose parent chain hits a permission wall, a symlink loop, or a non-directory segment. Only thrown when fs.realpathSync rejects with a code that is NOT 'ENOENT' — ENOENT falls through to the parent-dir fallback at line 372-378.
Common situations: Running the browse binary under a user that cannot traverse a directory in the --output-path; pointing --output-path at a symlink cycle; passing a path whose prefix is a file rather than a directory (ENOTDIR); SELinux/AppArmor denying realpath on the temp dir.
Related errors
- Cannot resolve real path: ${heatmapPath} (${err.code})
- Cannot resolve real path: ${filePath} (${err.code})
- Cannot resolve path: ${filePath}
- Skill "${name}" not found in any tier.
- Skill "${name}" not found.
AI-assisted analysis of garrytan/gstack@94993f7401 (2026-08-12).
Data as JSON: /api/errors/6da36ce7e4dfe844.
Report an issue: GitHub.