JuliusBrussee/caveman · error

cave_tool_sandbox_source_escapes_root

cave_tool_sandbox_source_escapes_root

Error message

cave_tool_sandbox_source_escapes_root

What it means

While staging sandbox sources, files discovered by expandSourceGraph are checked twice: first against the framework dist root (files under the framework dist are copied from there), then against the canonical sandbox root. This error is thrown when a graph source is neither within the framework dist nor within the sandbox root — i.e. a dependency graph expanded to a file outside every allowed root, so copying it would leak content from outside the sandbox.

Solutions

  1. Ensure the sandbox root encompasses all dependency sources: set root to the repo/monorepo root so resolved imports stay inside it.
  2. Install dependencies normally inside the sandbox root (avoid symlinked global stores) or vendor/copy them under the root before snapshotting.
  3. Check for symlinks in node_modules or the framework dist that resolve outside the root and replace them with real copies.
  4. If a dependency legitimately lives outside, add/expand the allowed roots (e.g. pass the correct frameworkDistRoot or bundle the dependency locally).

Example fix

// before
await snapshotSandboxSources('./packages/app/dist', entry, frameworkDist); // imports resolve to sibling packages outside
// after
await snapshotSandboxSources('/repo/root', entry, '/repo/root/node_modules/framework/dist'); // root covers all sources
Defensive patterns

Strategy: validation

Validate before calling

import { realpathSync, relativeSync } from 'path';
function sourceLiesInAllowedRoots(root: string, frameworkDist: string, source: string): boolean {
  const cr = realpathSync(root), cf = realpathSync(frameworkDist), cs = realpathSync(source);
  const inRoot = !relativeSync(cr, cs).startsWith('..');
  const inDist = !relativeSync(cf, cs).startsWith('..');
  return inRoot || inDist;
}
// pre-scan the dependency graph and fail fast on any source outside both roots

Try / catch

try {
  const snapshot = await buildSandboxSnapshot(root, entry, frameworkDistRoot);
} catch (err) {
  if (err instanceof Error && err.message === 'cave_tool_sandbox_source_escapes_root') {
    // widen root / vendor the escaping dependency, then retry
  } else throw err;
}

Prevention

When it happens

Trigger: expandSourceGraph follows imports/resolution (node_modules links, symlinks, tsconfig paths, monorepo cross-package imports) to a file whose path relative to both canonicalFrameworkDist and canonicalRoot escapes — e.g. a symlinked dependency resolving to a global store or a package outside the project root.

Common situations: pnpm/yarn PnP or npm symlinked node_modules resolving to a shared store outside the repo; monorepo imports referencing sibling packages outside the designated root; symlinked framework dist or hoisted deps pointing elsewhere on disk.

Understand the failure class

Background: Path traversal blocked: "path escapes the workspace" and "outside site root" errors when a path will not stay inside its allowed directory — this error's family across 26 libraries.

Related errors


AI-assisted analysis of JuliusBrussee/caveman@3ee70a1026 (2026-09-20). Data as JSON: /api/errors/24a403c0a52f9647. Report an issue: GitHub.

Appendix: source

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

    canonicalRoot,
    [canonicalEntry],
    false,
    [canonicalFrameworkDist],
  )].sort();
  const staging = await realpath(await mkdtemp(resolve(tmpdir(), "caveman-agent-source-")));
  try {
    const stagedFiles: string[] = [];
    await copyOptionalSandboxFile(
      resolve(canonicalRoot, "package.json"),
      resolve(staging, "package.json"),
      stagedFiles,
    );
    for (const source of graph) {
      if (!escapesRoot(relative(canonicalFrameworkDist, source))) {
        continue;
      }
      const name = relative(canonicalRoot, source);
      if (escapesRoot(name)) throw new Error("cave_tool_sandbox_source_escapes_root");
      const destination = resolve(staging, name);
      await mkdir(dirname(destination), { recursive: true });
      await copyFile(source, destination);
      stagedFiles.push(destination);
    }
    const frameworkName = relative(canonicalRoot, canonicalFrameworkDist);
    if (!escapesRoot(frameworkName)) {
      const frameworkDestination = resolve(staging, frameworkName);
      await mkdir(dirname(frameworkDestination), { recursive: true });
      await symlink(
        canonicalFrameworkDist,
        frameworkDestination,
        process.platform === "win32" ? "junction" : "dir",
      );
      stagedFiles.push(frameworkDestination);
    }
    const nodeModules = await realpath(resolve(canonicalRoot, "node_modules"));
    await symlink(

View on GitHub (pinned to 3ee70a1026)