JuliusBrussee/caveman · error
cave_sandbox_source_read_grant_escapes_staging
cave_sandbox_source_read_grant_escapes_staging
Error message
cave_sandbox_source_read_grant_escapes_staging
What it means
The common ancestor computed for the collapsed --allow-fs-read grant lies outside the resolved staging root (escapesRoot(relative(stagingRoot, ancestor))). A grant anchored there would read paths beyond the per-run staged copy - the staged copy exists precisely so the sandbox never reads the real project root with its .env and credentials - so the call refuses rather than widen the grant.
Source
Thrown at packages/agent/src/runtime.ts:4919
}
export function sandboxSourceReadFlags(
sourceFiles: readonly string[],
stagingRoot?: string,
): string[] {
if (sourceFiles.length <= SANDBOX_FS_READ_FLAG_THRESHOLD) {
return sourceFiles.map((path) => `--allow-fs-read=${path}`);
}
if (stagingRoot === undefined) {
throw new Error("cave_sandbox_source_staging_root_required");
}
const resolvedStagingRoot = resolve(stagingRoot);
if (dirname(resolvedStagingRoot) === resolvedStagingRoot) {
throw new Error("cave_sandbox_source_read_root_refused");
}
const ancestor = commonAncestorDir(sourceFiles);
if (escapesRoot(relative(resolvedStagingRoot, ancestor))) {
throw new Error("cave_sandbox_source_read_grant_escapes_staging");
}
return [`--allow-fs-read=${ancestor}`];
}
/** Decode the length-prefixed result frame delivered on the worker's fd 3. */
function decodeResultFrame(
buffer: Buffer,
): { ok: boolean; value?: unknown; code?: string } | undefined {
if (buffer.byteLength < 4) return undefined;
const length = buffer.readUInt32BE(0);
if (buffer.byteLength < 4 + length) return undefined;
try {
return JSON.parse(buffer.subarray(4, 4 + length).toString("utf8"));
} catch {
return undefined;
}
}
View on GitHub (pinned to 766dce6b13)
Solutions
- Ensure every entry of sourceFiles is the staged copy inside the per-run staging root
- Validate each path is inside stagingRoot before the collapse (the executor does this; direct callers must too)
- Keep the file count at or below 1024 so per-file flags are emitted and no collapse/ancestor logic runs
- Re-stage the source graph if staging produced out-of-root copies
Example fix
// before: raw project paths leak into the list sandboxSourceReadFlags(['/home/me/proj/src/a.ts', ...], stagingRoot); // after: only staged copies sandboxSourceReadFlags(stagedFilesUnder(stagingRoot), stagingRoot);
Defensive patterns
Strategy: validation
Validate before calling
// Every source file must live inside the staging root before collapsing
import { relative, resolve } from 'node:path';
function allInsideStaging(files: readonly string[], stagingRoot: string): boolean {
const root = resolve(stagingRoot);
return files.every((f) => { const r = relative(root, resolve(f)); return r !== '' && !r.startsWith('..'); });
} Try / catch
try {
flags = sandboxSourceReadFlags(files, stagingRoot);
} catch (error) {
if (error instanceof Error && error.message === 'cave_sandbox_source_read_grant_escapes_staging') {
// a raw project path slipped in: re-stage the graph, then retry with staged copies only
}
throw error;
} Prevention
- Pass only the staged copies produced by the executor - never original project paths
- Validate containment (no '..' relative to stagingRoot) before the collapse path runs
- Keep file lists at or below 1024 so per-file flags avoid the ancestor computation
- Beware symlinks resolving outside the staging root
When it happens
Trigger: sourceFiles containing raw project paths instead of the staged copies under stagingRoot; one stray path (or a symlink resolving outside) pulling the common ancestor above the staging root; staging that left some files outside the per-run workspace.
Common situations: Passing original workspace paths to the flag builder instead of the staged graph; symlinked dependencies resolving outside tmpdir; partial staging failures leaving a mixed list.
Related errors
- caveman agent: dev ${kind} escapes project root
- caveman agent: dev ${kind} symlink escapes project root
- cave_sandbox_source_staging_root_required
- cave_sandbox_source_read_root_refused
- caveman build: config must use strict lock and required sand
AI-assisted analysis of JuliusBrussee/caveman@766dce6b13 (2026-08-18).
Data as JSON: /api/errors/b856fd189cd48e28.
Report an issue: GitHub.