JuliusBrussee/caveman · error

cave_sandbox_source_read_root_refused

cave_sandbox_source_read_root_refused

Error message

cave_sandbox_source_read_root_refused

What it means

While walking up from the first staged file's directory toward a common ancestor for the collapsed --allow-fs-read grant, the walk reached the filesystem root (parent === ancestor) while some path was still judged to escape it. Since nothing can escape '/', this branch indicates a path/normalization inconsistency and is refused rather than granting a read rooted at the entire filesystem. Defensive invariant guard.

Source

Thrown at packages/agent/src/runtime.ts:4893

 * files to their common ancestor directory. A large project would
 * otherwise blow the OS argument limit (E2BIG) and the tool could not spawn at
 * all. The collapse is safe here: `sourceFiles` are paths inside the per-run
 * STAGED COPY, which already contains only the reachable source graph — never
 * the real project root with its .env and credentials.
 */
const SANDBOX_FS_READ_FLAG_THRESHOLD = 1024;

function commonAncestorDir(paths: readonly string[]): string {
  const dirs = paths.map((path) => resolve(dirname(path)));
  const first = dirs[0];
  if (first === undefined) {
    throw new Error("cave_sandbox_source_staging_root_required");
  }
  let ancestor = first;
  while (dirs.some((path) => escapesRoot(relative(ancestor, path)))) {
    const parent = dirname(ancestor);
    if (parent === ancestor) {
      throw new Error("cave_sandbox_source_read_root_refused");
    }
    ancestor = parent;
  }
  if (dirname(ancestor) === ancestor) {
    throw new Error("cave_sandbox_source_read_root_refused");
  }
  return ancestor;
}

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");

View on GitHub (pinned to 766dce6b13)

Solutions

  1. Pass consistently resolved absolute paths (resolve() each path first)
  2. Use the staged copies produced by the executor instead of hand-built path lists
  3. If hit through the public API, capture the sourceFiles list and report it as a staging bug
Defensive patterns

Strategy: validation

Validate before calling

// Pre-check: consistently resolved absolute paths sharing a non-root ancestor
import { resolve, dirname } from 'node:path';
function precheckSourceFiles(files: readonly string[]): void {
  const dirs = files.map((f) => resolve(dirname(f)));
  if (new Set(dirs.map((d) => d.split('/')[1])).size > 1) {
    throw new Error('source files span multiple top-level trees; re-stage under one root');
  }
}

Try / catch

try {
  flags = sandboxSourceReadFlags(sourceFiles, stagingRoot);
} catch (error) {
  if (error instanceof Error && error.message === 'cave_sandbox_source_read_root_refused') {
    // ancestor walk hit the filesystem root: capture the path list and report a staging bug
  }
  throw error;
}

Prevention

When it happens

Trigger: Path normalization mismatches (mixed absolute/relative or symlink-resolved inputs) passed directly into commonAncestorDir; synthetic or malformed paths in tests; not reachable via the public executor with a correctly staged graph.

Common situations: Direct imports of commonAncestorDir with hand-built path lists; platform-specific path separators (Windows backslash vs POSIX) mixed into one list; internal bug in staging.

Related errors


AI-assisted analysis of JuliusBrussee/caveman@766dce6b13 (2026-08-18). Data as JSON: /api/errors/847c94030e377d81. Report an issue: GitHub.