JuliusBrussee/caveman · error
cave_tool_sandbox_entry_escapes_root
Error message
cave_tool_sandbox_entry_escapes_root
What it means
Thrown by stageSandboxSourceGraph when the canonicalized (realpath) entry file does not live under the canonicalized sandbox root. Both paths go through realpath first, so symlink indirection cannot hide the escape.
Source
Thrown at packages/agent/src/runtime.ts:2270
if (activeExecution) await activeExecution.catch(() => undefined);
if (activeAbort) options.signal?.removeEventListener("abort", activeAbort);
releaseConversation();
// Idempotent backstop for the abort/return() paths that never reached a
// terminal yield; the success and error paths already released above.
await releaseRunResources();
}
}
async function stageSandboxSourceGraph(
root: string,
entryPath: string,
frameworkDistRoot: string,
): Promise<SandboxSourceSnapshot> {
const canonicalRoot = await realpath(root);
const canonicalEntry = await realpath(entryPath);
const entryName = relative(canonicalRoot, canonicalEntry);
if (escapesRoot(entryName)) {
throw new Error("cave_tool_sandbox_entry_escapes_root");
}
const canonicalFrameworkDist = await realpath(frameworkDistRoot);
const graph = [...await expandSourceGraph(
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))) {View on GitHub (pinned to 27d5a3981a)
Solutions
- Pass an entryPath that genuinely lives inside root after symlink resolution — resolve both with realpath yourself before calling
- Repoint the symlink at a file inside the sandbox root, or copy the entry in
- If the entry legitimately lives elsewhere, stage the graph from a root that actually contains it
Example fix
// before stageSandboxSourceGraph(root, resolve(root, "linked-entry.ts"), dist); // symlink out // after const realRoot = await realpath(root); const entry = await realpath(resolve(root, "src/main.ts")); stageSandboxSourceGraph(realRoot, entry, dist);
Defensive patterns
Strategy: validation
Validate before calling
import { realpath, relative } from "node:fs/promises";
async function entryWithinRoot(root: string, entry: string): Promise<boolean> {
const r = await realpath(root);
const rel = relative(r, await realpath(entry));
return rel !== "" && !rel.startsWith("..") && !isAbsolute(rel);
} Type guard
function isEntryEscapesRoot(e: unknown): e is Error {
return e instanceof Error && e.message === "cave_tool_sandbox_entry_escapes_root";
} Try / catch
try {
await stageSandboxSourceGraph(root, entry, dist);
} catch (e) {
if (isEntryEscapesRoot(e)) { /* resolve real paths and pick an in-root entry */ }
else throw e;
} Prevention
- Always realpath both root and entry before staging
- Avoid symlinked entry files in sandboxed tools
- Assert containment (relative path not starting with '..') as a pre-condition in your own code
When it happens
Trigger: entryPath, after resolving symlinks, resolves outside root — e.g. entry is a symlink pointing elsewhere, or root itself contains a symlinked parent making the entry's real location external.
Common situations: Passing a symlinked entry (e.g. from a node_modules link or /tmp symlink); mixing realpathed and non-realpathed path inputs; monorepo where the entry is linked from another package.
Related errors
- cave_tool_sandbox_source_escapes_root
- caveman-code: path escapes the workspace: ${candidate}
- probe returned false
- cave_sandbox_conformance_failed
- cave_live_eval_sandbox_profile_escapes_root
AI-assisted analysis of JuliusBrussee/caveman@27d5a3981a (2026-08-15).
Data as JSON: /api/errors/c88c2b3b1ebc1fe2.
Report an issue: GitHub.