JuliusBrussee/caveman · error
caveman-code: path escapes the workspace
Error message
caveman-code: path escapes the workspace: ${candidate} What it means
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.
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
Example fix
// before
await code.run({ workspace: "/work/app" }, { path: "../secrets/key.pem" });
// after
await code.run({ workspace: "/work" }, { path: "app/src/main.ts" }); Defensive patterns
Strategy: validation
Validate before calling
import { resolve, relative, isAbsolute } from "node:path";
function staysInWorkspace(workspace: string, candidate: string): boolean {
const rel = relative(resolve(workspace), resolve(workspace, candidate));
return !(rel === ".." || rel.startsWith("../") || isAbsolute(rel));
}
if (!staysInWorkspace(ws, p)) throw new Error(`refusing out-of-workspace path: ${p}`); Type guard
const isInsideWorkspace = (rel: string): boolean =>
rel !== ".." && !rel.startsWith("../") && !rel.startsWith("..\\") && !isAbsolute(rel); Try / catch
try {
const full = await containedPath(ws, candidate);
} catch (err) {
if ((err as Error).message.includes("path escapes the workspace")) {
console.error(`Rejected path ${candidate}; use a path inside the workspace`);
} else throw err;
} Prevention
- 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
When it happens
Trigger: 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.
Common situations: 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.
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
- ambiguous escaped path sequence
- backslash is not allowed in path
- cave_sandbox_source_read_root_refused
- cave_tool_sandbox_entry_escapes_root
- caveman agent: dev escapes project root
AI-assisted analysis of JuliusBrussee/caveman@3ee70a1026 (2026-09-20).
Data as JSON: /api/errors/438996adc48b8ffc.
Report an issue: GitHub.
Appendix: source
Thrown at packages/agent/src/code.ts:386
);
},
});
return [readFileTool, grepTool, bashTool, editTool];
}
/**
* Resolve a caller path against the canonical workspace and refuse anything
* that lands outside it.
*
* A lexical prefix check is not containment: a symlink inside the workspace
* pointing anywhere on the filesystem passes it. Both sides are canonicalized
* first, matching how `stageSandboxSourceGraph` decides the same question.
*/
async function containedPath(canonicalWorkspace: string, candidate: string): Promise<string> {
const full = await canonicalizePath(resolve(canonicalWorkspace, candidate));
if (escapesRoot(relative(canonicalWorkspace, full))) {
throw new Error(`caveman-code: path escapes the workspace: ${candidate}`);
}
return full;
}
function escapesRoot(path: string): boolean {
return path === ".." || path.startsWith("../") || path.startsWith("..\\") || isAbsolute(path);
}
/**
* `realpath` for a path whose leaf may not exist yet (a file `edit_file` is
* about to create): canonicalize the deepest existing ancestor and re-attach
* the missing tail, so every symlink on the existing part is still resolved.
*/
async function canonicalizePath(target: string): Promise<string> {
const missing: string[] = [];
let current = target;
for (;;) {
try {View on GitHub (pinned to 3ee70a1026)