{"record":{"id":"438996adc48b8ffc","repo":"JuliusBrussee/caveman","slug":"caveman-code-path-escapes-the-workspace-candid","errorCode":null,"errorMessage":"caveman-code: path escapes the workspace: ${candidate}","messagePattern":"caveman-code: path escapes the workspace: (.+?)","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"packages/agent/src/code.ts","lineNumber":386,"sourceCode":"      );\n    },\n  });\n\n  return [readFileTool, grepTool, bashTool, editTool];\n}\n\n/**\n * Resolve a caller path against the canonical workspace and refuse anything\n * that lands outside it.\n *\n * A lexical prefix check is not containment: a symlink inside the workspace\n * pointing anywhere on the filesystem passes it. Both sides are canonicalized\n * first, matching how `stageSandboxSourceGraph` decides the same question.\n */\nasync function containedPath(canonicalWorkspace: string, candidate: string): Promise<string> {\n  const full = await canonicalizePath(resolve(canonicalWorkspace, candidate));\n  if (escapesRoot(relative(canonicalWorkspace, full))) {\n    throw new Error(`caveman-code: path escapes the workspace: ${candidate}`);\n  }\n  return full;\n}\n\nfunction escapesRoot(path: string): boolean {\n  return path === \"..\" || path.startsWith(\"../\") || path.startsWith(\"..\\\\\") || isAbsolute(path);\n}\n\n/**\n * `realpath` for a path whose leaf may not exist yet (a file `edit_file` is\n * about to create): canonicalize the deepest existing ancestor and re-attach\n * the missing tail, so every symlink on the existing part is still resolved.\n */\nasync function canonicalizePath(target: string): Promise<string> {\n  const missing: string[] = [];\n  let current = target;\n  for (;;) {\n    try {","sourceCodeStart":368,"sourceCodeEnd":404,"githubUrl":"https://github.com/JuliusBrussee/caveman/blob/3ee70a102609e550bd2e68004bf5990a9341c851/packages/agent/src/code.ts#L368-L404","documentation":"containedPath validates that a user-supplied path candidate stays inside the canonicalized workspace root. Both workspace and candidate are canonicalized, then escapesRoot checks the relative path; if the candidate resolves outside the root (or is an absolute path that escapes), the library throws this error to enforce sandbox containment of all file operations.","triggerScenarios":"Calling any caveman-code API that takes a path (e.g. staging source files) with a path containing '../' segments that climb out of the workspace, a symlink pointing outside the workspace, or an absolute path outside the workspace.","commonSituations":"Passing an absolute path from elsewhere on disk while CWD differs from the workspace; a temp dir symlink on macOS (/tmp -> /private/tmp) making a seemingly-inside path canonicalize outside; using '..' to reference sibling projects.","solutions":["Pass paths relative to the workspace root and ensure they resolve inside it","Resolve symlinks: pass the real path so canonicalization keeps it inside the workspace","Set the workspace option to a parent directory that actually contains the target path"],"exampleFix":"// before\nawait code.run({ workspace: \"/work/app\" }, { path: \"../secrets/key.pem\" });\n// after\nawait code.run({ workspace: \"/work\" }, { path: \"app/src/main.ts\" });","handlingStrategy":"validation","validationCode":"import { resolve, relative, isAbsolute } from \"node:path\";\nfunction staysInWorkspace(workspace: string, candidate: string): boolean {\n  const rel = relative(resolve(workspace), resolve(workspace, candidate));\n  return !(rel === \"..\" || rel.startsWith(\"../\") || isAbsolute(rel));\n}\nif (!staysInWorkspace(ws, p)) throw new Error(`refusing out-of-workspace path: ${p}`);","typeGuard":"const isInsideWorkspace = (rel: string): boolean =>\n  rel !== \"..\" && !rel.startsWith(\"../\") && !rel.startsWith(\"..\\\\\") && !isAbsolute(rel);","tryCatchPattern":"try {\n  const full = await containedPath(ws, candidate);\n} catch (err) {\n  if ((err as Error).message.includes(\"path escapes the workspace\")) {\n    console.error(`Rejected path ${candidate}; use a path inside the workspace`);\n  } else throw err;\n}","preventionTips":["Always pass workspace-relative paths into the API","Canonicalize/symlink-resolve paths (fs.realpath) before use, especially /tmp on macOS","Keep secret files outside any directory you register as the workspace"],"tags":["filesystem","security","path-validation"],"backgroundTag":"path-traversal-blocked","analyzedSha":"3ee70a102609e550bd2e68004bf5990a9341c851","analyzedAt":"2026-09-20T15:53:39.229Z","contentChangedAt":"2026-09-20T15:53:39.229Z","schemaVersion":2},"datasetVersion":"2026-09-23T08:17:48.524Z"}