JuliusBrussee/caveman · critical · Error
caveman-code: path escapes the workspace: ${candidate}
Error message
caveman-code: path escapes the workspace: ${candidate} What it means
Every tool path is resolved against the canonical (realpath'd) workspace and rejected if it lands outside it. This is a directory-traversal/symlink-escape guard: a lexical prefix check would pass a workspace symlink pointing at /etc, so both sides are canonicalized before the relative-path containment test. It mirrors the same check used when staging the sandbox source graph.
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 27d5a3981a)
Solutions
- Keep all read/write/glob/grep targets inside the workspace; reference files by paths relative to the workspace root
- If a symlink inside the workspace points outside, either remove the symlink or copy the target file into the workspace
- If the external file is legitimately needed, run the agent with a workspace root that contains it (choose a higher-level root directory)
- Never pass absolute paths unless they resolve inside the canonical workspace root
Example fix
# before: symlink escape
ln -s /etc/passwd workspace/passwd-link
await read_file('passwd-link') // throws
# after: keep content inside the workspace
cp /etc/passwd workspace/reference-passwd.txt
await read_file('reference-passwd.txt') Defensive patterns
Strategy: type-guard
Validate before calling
import { resolve, relative, isAbsolute } from "node:path";
import { realpath } from "node:fs/promises";
async function assertInsideWorkspace(root: string, p: string): Promise<string> {
const canonRoot = await realpath(root);
const full = await realpath(resolve(canonRoot, p)).catch(() => resolve(canonRoot, p));
const rel = relative(canonRoot, full);
if (rel === ".." || rel.startsWith("../") || rel.startsWith("..\\") || isAbsolute(rel)) {
throw new Error(`refusing outside-workspace path: ${p}`);
}
return full;
} Type guard
function isWorkspaceRelative(candidate: string): boolean {
const norm = candidate.replace(/\\/g, "/");
return !norm.startsWith("/") &&
!norm.split("/").includes("..") &&
/^[^/]/.test(norm) &&
!/^[A-Za-z]:/.test(norm);
} Try / catch
try {
await readTool.execute({ path }, signal);
} catch (err) {
if (err instanceof Error && err.message.includes("escapes the workspace")) {
// security boundary: log and reject the request; never retry with a mutated path
auditLog.warn("path escape attempt", { path });
throw err;
}
throw err;
} Prevention
- Resolve and canonicalize (realpath) every caller-supplied path before handing it to tools
- Audit the workspace for symlinks pointing outside the tree before starting a session
- Treat occurrences of this error as attempted escapes worth logging, not as transient failures
When it happens
Trigger: Passing '../secrets.env' or an absolute path outside the workspace; passing a path that traverses a symlink INSIDE the workspace whose target lives elsewhere on disk; a workspace whose canonical root differs from the apparent one (macOS /tmp -> /private/tmp).
Common situations: Agents attempting to read ~/.ssh or /etc/hosts via relative traversal; repos containing node_modules symlinks or other linked directories pointing outside the tree; monorepo tooling that assumes a parent directory is accessible.
Related errors
- cave_live_eval_sandbox_profile_escapes_root
- caveman agent: file source escapes project root
- cave_host_sandbox_nested_under_required
- cave_tool_sandbox_entry_escapes_root
- cave_tool_sandbox_source_escapes_root
AI-assisted analysis of JuliusBrussee/caveman@27d5a3981a (2026-08-15).
Data as JSON: /api/errors/438996adc48b8ffc.
Report an issue: GitHub.