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
- Ensure the sandbox root encompasses all dependency sources: set root to the repo/monorepo root so resolved imports stay inside it.
- Install dependencies normally inside the sandbox root (avoid symlinked global stores) or vendor/copy them under the root before snapshotting.
- Check for symlinks in node_modules or the framework dist that resolve outside the root and replace them with real copies.
- 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
- Use real (copied) node_modules inside the sandbox rather than symlinked package-manager stores.
- Set the sandbox root at the monorepo/repo level so cross-package imports stay inside it.
- Audit symlinks in node_modules and the framework dist before snapshotting (find -type l).
- Keep dependency resolution (tsconfig paths, bundler aliases) pointed inside the sandbox root.
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
- cave_live_eval_sandbox_profile_escapes_root
- cave_sandbox_source_read_grant_escapes_staging
- cave_tool_sandbox_entry_escapes_root
- cave_host_sandbox_nested_under_required
- cave_sandbox_child_process_containment_unavailable
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)