JuliusBrussee/caveman · error
caveman agent: dev escapes project root
Error message
caveman agent: dev ${kind} escapes project root What it means
During dev-module loading, copyProjectFile stages a project file into a temporary staging directory and refuses any file whose path resolves outside the project root. The library throws this to prevent dev tooling from reading or copying files outside the project. It fires before any filesystem copy happens, as a path-traversal guard on the relative path computed from root.
Solutions
- Move or symlink the referenced file inside the project root so relative(root, source) stays within root.
- Fix the dev-loader config (include globs/file sources) to reference only paths under the project root.
- If the file genuinely lives outside root, copy it into the project or use a proper workspace/package link instead of a raw path.
- Check that root passed to the loader is the actual intended project root (e.g. package dir, not repo root or vice versa).
Example fix
// before
await includeFiles(agent, ["/tmp/shared/extra.ts"]);
// after
cp("/tmp/shared/extra.ts", "./vendor/extra.ts");
await includeFiles(agent, ["./vendor/extra.ts"]); Defensive patterns
Strategy: validation
Validate before calling
import { relative, isAbsolute, resolve } from "node:path";
function pathInsideRoot(root: string, source: string): boolean {
const abs = isAbsolute(source) ? source : resolve(root, source);
const rel = relative(root, abs);
return rel !== "" && !rel.startsWith("..") && !isAbsolute(rel);
}
// call pathInsideRoot(root, file) for each file before loadDevModule/includeFiles Type guard
function isInsideRoot(root: string, source: string): boolean {
const rel = relative(root, source);
return rel !== "" && !rel.startsWith("..") && !isAbsolute(rel);
} Try / catch
try {
await loadDevModule(entry);
} catch (err) {
if (err instanceof Error && err.message.includes("escapes project root")) {
// surface the offending path from config and fail fast with a clear message
throw new Error(`dev-loader config references a path outside the project root: ${err.message}`);
}
throw err;
} Prevention
- Keep all dev-include sources under the package root and use relative paths in config.
- Audit include globs when moving code into or out of a monorepo.
- Realpath the loader root at startup so comparisons are consistent.
- Add a preflight check that iterates configured include files and asserts isInsideRoot for each.
When it happens
Trigger: Calling loadDevModule, includeFiles, includeOptionalFiles, includeSourceGraph, or includeAgentFiles with a source path that is not inside the loader's project root, e.g. an absolute path in another directory or a '../'-relative path that climbs above root.
Common situations: Dev config pointing include globs at a shared monorepo directory outside the package root; a symlinked node_modules or workspace layout where source files are resolved from a sibling package; passing an absolute path from a temp dir into includeFiles.
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
- cave_tool_sandbox_entry_escapes_root
- caveman agent: dev symlink escapes project root
- caveman agent: file source escapes project root
- cave_live_eval_sandbox_profile_escapes_root
- cave_sandbox_source_read_grant_escapes_staging
AI-assisted analysis of JuliusBrussee/caveman@3ee70a1026 (2026-09-20).
Data as JSON: /api/errors/c505457db6544590.
Report an issue: GitHub.
Appendix: source
Thrown at packages/agent/src/dev-loader.ts:146
async function copyOptional(source: string, destination: string): Promise<void> {
try {
await readFile(source);
await mkdir(dirname(destination), { recursive: true });
await copyFile(source, destination);
} catch (error) {
if ((error as NodeJS.ErrnoException).code !== "ENOENT") throw error;
}
}
async function copyProjectFile(
root: string,
staging: string,
source: string,
kind: "source graph" | "file source",
): Promise<void> {
const name = relative(root, source);
if (escapesRoot(name)) {
throw new Error(`caveman agent: dev ${kind} escapes project root`);
}
const canonicalSource = await realpath(source);
if (escapesRoot(relative(root, canonicalSource))) {
throw new Error(`caveman agent: dev ${kind} symlink escapes project root`);
}
const destination = resolve(staging, name);
await mkdir(dirname(destination), { recursive: true });
await copyFile(canonicalSource, destination);
}
function remapDevError(error: unknown, staging: string, root: string): unknown {
if (!(error instanceof Error)) return error;
const mapped = new Error(error.message, { cause: error.cause });
mapped.name = error.name;
mapped.stack = (error.stack ?? `${error.name}: ${error.message}`).replaceAll(staging, root);
return mapped;
}
View on GitHub (pinned to 3ee70a1026)