JuliusBrussee/caveman · error
cave_tool_sandbox_entry_escapes_root
cave_tool_sandbox_entry_escapes_root
Error message
cave_tool_sandbox_entry_escapes_root
What it means
When building a sandbox source snapshot, the library canonicalizes (realpath) the sandbox root and the requested entry file and computes the entry's path relative to the root. If that relative path escapes the root (e.g. contains '..' segments or is absolute), it throws this error to block reading files outside the sandbox. This is a security guard against symlink- or path-based escape from the sandbox root.
Solutions
- Validate that the entry path resolves inside the sandbox root before calling: compare path.relative(realpath(root), realpath(entry)) and reject values starting with '..' or absolute.
- Copy/symlink the entry file into the sandbox root rather than referencing it externally.
- Sanitize/reject user input containing '..' or absolute paths before it reaches the entry parameter.
- Remove or replace out-of-root symlinks inside the sandbox root that redirect the entry elsewhere.
Example fix
// before
await buildSandboxSnapshot(root, userInputPath, frameworkDistRoot); // may be anywhere
// after
const realEntry = await realpath(userInputPath);
const rel = relative(await realpath(root), realEntry);
if (rel.startsWith('..') || isAbsolute(rel)) throw new Error('entry must live inside sandbox root');
await buildSandboxSnapshot(root, realEntry, frameworkDistRoot); Defensive patterns
Strategy: validation
Validate before calling
import { realpath, relative, isAbsolute } from 'path';
async function entryIsInsideRoot(root: string, entry: string): Promise<boolean> {
const [cr, ce] = await Promise.all([realpath(root), realpath(entry)]);
const rel = relative(cr, ce);
return rel !== '' && !rel.startsWith('..') && !isAbsolute(rel);
}
// call before snapshotting: if (!(await entryIsInsideRoot(root, entryPath))) reject; Try / catch
try {
const snapshot = await buildSandboxSnapshot(root, entryPath, frameworkDistRoot);
} catch (err) {
if (err instanceof Error && err.message === 'cave_tool_sandbox_entry_escapes_root') {
// reject the request / copy the entry into the sandbox root and retry
} else throw err;
} Prevention
- Never pass raw user input as the entry path; resolve and contain it under the sandbox root first.
- Treat symlinks as hostile: realpath the entry (and everything under root) before trusting it.
- Add tests covering '../' traversal and absolute paths in the entry parameter.
- Keep the sandbox root as the sole mount point for user-accessible files.
When it happens
Trigger: Calling the sandbox snapshot function with an entryPath that resolves (after realpath/symlink resolution) outside the canonical sandbox root — e.g. an absolute path elsewhere on disk, '../' traversal, or a symlink inside root pointing to an external file.
Common situations: Passing a user-supplied file path directly as the entry without validating it's inside the sandbox; symlinks created at runtime that point out of the staging dir; constructing entry paths by string concatenation with untrusted input.
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_sandbox_source_read_root_refused
- cave_tool_sandbox_source_escapes_root
- caveman agent: dev escapes project root
AI-assisted analysis of JuliusBrussee/caveman@3ee70a1026 (2026-09-20).
Data as JSON: /api/errors/c88c2b3b1ebc1fe2.
Report an issue: GitHub.
Appendix: 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 3ee70a1026)