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

  1. 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.
  2. Copy/symlink the entry file into the sandbox root rather than referencing it externally.
  3. Sanitize/reject user input containing '..' or absolute paths before it reaches the entry parameter.
  4. 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

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


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)