garrytan/gstack · error · Error
Path must be within: ${safeDirs.join(', ')}
Error message
Path must be within: ${safeDirs.join(', ')} What it means
Thrown by the annotated-screenshot sandbox check in snapshot.ts. After resolving the realpath of the requested output path, the code asserts it lives inside one of the safe directories (TEMP_DIR or process.cwd()). If realpath escaped both — e.g. via a symlink or an absolute --output-path elsewhere on disk — the write is refused before page.screenshot() is ever called.
Source
Thrown at browse/src/snapshot.ts:384
});
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;
}
}
await page.evaluate((boxes) => {View on GitHub (pinned to 94993f7401)
Solutions
- Point --output-path at a location physically inside the current working directory (verify with `readlink -f`).
- Remove or rewrite symlinks in the output path that escape the safe dirs.
- Launch the browse process from the directory you want outputs to land in so process.cwd() covers it.
- Set TMPDIR to a writable temp area you control if you need the TEMP_DIR branch.
Example fix
// before
snapshot(page, { annotate: true, outputPath: '/var/www/shot.png' }); // outside cwd & TEMP_DIR
// after
snapshot(page, { annotate: true, outputPath: './shots/shot.png' }); // inside cwd Defensive patterns
Strategy: validation
Validate before calling
const fs = require('fs');
const path = require('path');
function assertPathWithin(p: string, safeDirs: string[]): void {
const real = fs.realpathSync(p); // throws if not resolvable
const ok = safeDirs.some(d => real === d || real.startsWith(d + path.sep));
if (!ok) throw new Error(`Path must be within: ${safeDirs.join(', ')}`);
} Try / catch
try {
await snapshot(session, { annotate: true, outputPath });
} catch (e: any) {
if (/^Path must be within:/.test(e.message)) {
outputPath = path.join(process.cwd(), 'browse-annotated.png');
await snapshot(session, { annotate: true, outputPath });
} else throw e;
} Prevention
- Resolve --output-path with path.resolve against process.cwd() so it always lands inside the sandbox.
- Avoid symlinks in the output path or resolve them first with fs.realpathSync.
- Document the safe-dir list for your team so callers know where outputs may live.
- Treat the sandbox as a hard contract — do not attempt to widen it from caller code.
When it happens
Trigger: Passing opts.outputPath (or letting it default to an absolute path) whose realpath resolves outside [TEMP_DIR, process.cwd()]; a symlink inside cwd that points to /etc or another tree; running the CLI from a working directory that itself symlinks elsewhere after realpathSync.
Common situations: Hard-coding an --output-path like '/tmp/other-tool/shot.png' where /tmp/other-tool is a symlink to /var/www; chaining snapshots across processes that share a cwd but write to different roots; container mounts where cwd resolves through a symlink to a host path.
Related errors
- Path must be within: ${SAFE_DIRECTORIES.join(', ')}
- Path must be within: ${TEMP_ONLY.join(', ')} (remote file se
- State name must be alphanumeric (a-z, 0-9, _, -)
- Path traversal sequences (..) are not allowed
- image resolves OUTSIDE the input directory: ${src} → ${realF
AI-assisted analysis of garrytan/gstack@94993f7401 (2026-08-12).
Data as JSON: /api/errors/16f4206d0c6cf9dc.
Report an issue: GitHub.