{"record":{"id":"c88c2b3b1ebc1fe2","repo":"JuliusBrussee/caveman","slug":"cave-tool-sandbox-entry-escapes-root","errorCode":null,"errorMessage":"cave_tool_sandbox_entry_escapes_root","messagePattern":"cave_tool_sandbox_entry_escapes_root","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"packages/agent/src/runtime.ts","lineNumber":2270,"sourceCode":"    if (activeExecution) await activeExecution.catch(() => undefined);\n    if (activeAbort) options.signal?.removeEventListener(\"abort\", activeAbort);\n    releaseConversation();\n    // Idempotent backstop for the abort/return() paths that never reached a\n    // terminal yield; the success and error paths already released above.\n    await releaseRunResources();\n  }\n}\n\nasync function stageSandboxSourceGraph(\n  root: string,\n  entryPath: string,\n  frameworkDistRoot: string,\n): Promise<SandboxSourceSnapshot> {\n  const canonicalRoot = await realpath(root);\n  const canonicalEntry = await realpath(entryPath);\n  const entryName = relative(canonicalRoot, canonicalEntry);\n  if (escapesRoot(entryName)) {\n    throw new Error(\"cave_tool_sandbox_entry_escapes_root\");\n  }\n  const canonicalFrameworkDist = await realpath(frameworkDistRoot);\n  const graph = [...await expandSourceGraph(\n    canonicalRoot,\n    [canonicalEntry],\n    false,\n    [canonicalFrameworkDist],\n  )].sort();\n  const staging = await realpath(await mkdtemp(resolve(tmpdir(), \"caveman-agent-source-\")));\n  try {\n    const stagedFiles: string[] = [];\n    await copyOptionalSandboxFile(\n      resolve(canonicalRoot, \"package.json\"),\n      resolve(staging, \"package.json\"),\n      stagedFiles,\n    );\n    for (const source of graph) {\n      if (!escapesRoot(relative(canonicalFrameworkDist, source))) {","sourceCodeStart":2252,"sourceCodeEnd":2288,"githubUrl":"https://github.com/JuliusBrussee/caveman/blob/27d5a3981a347890211bb1bf2439e5c821a63bc9/packages/agent/src/runtime.ts#L2252-L2288","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","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"],"exampleFix":"// before\nstageSandboxSourceGraph(root, resolve(root, \"linked-entry.ts\"), dist); // symlink out\n\n// after\nconst realRoot = await realpath(root);\nconst entry = await realpath(resolve(root, \"src/main.ts\"));\nstageSandboxSourceGraph(realRoot, entry, dist);","handlingStrategy":"validation","validationCode":"import { realpath, relative } from \"node:fs/promises\";\nasync function entryWithinRoot(root: string, entry: string): Promise<boolean> {\n  const r = await realpath(root);\n  const rel = relative(r, await realpath(entry));\n  return rel !== \"\" && !rel.startsWith(\"..\") && !isAbsolute(rel);\n}","typeGuard":"function isEntryEscapesRoot(e: unknown): e is Error {\n  return e instanceof Error && e.message === \"cave_tool_sandbox_entry_escapes_root\";\n}","tryCatchPattern":"try {\n  await stageSandboxSourceGraph(root, entry, dist);\n} catch (e) {\n  if (isEntryEscapesRoot(e)) { /* resolve real paths and pick an in-root entry */ }\n  else throw e;\n}","preventionTips":["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"],"tags":["sandbox","filesystem","symlink","security"],"backgroundTag":null,"analyzedSha":"27d5a3981a347890211bb1bf2439e5c821a63bc9","analyzedAt":"2026-08-15T09:26:11.751Z","schemaVersion":2},"datasetVersion":"2026-08-15T17:31:12.345Z"}