JuliusBrussee/caveman · error · Error
caveman agent: dev ${kind} escapes project root
Error message
caveman agent: dev ${kind} escapes project root What it means
Thrown by copyProjectFile in the dev loader (packages/agent/src/dev-loader.ts:146): while staging the immutable dev snapshot, a project-relative source path (a FileSource instruction/context file or a source-graph module) resolves outside the project root. Dev staging only copies files under the project root, so any '..'-escaping or absolute path is rejected.
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 27d5a3981a)
Solutions
- Move the referenced file inside the project root and reference it with a root-relative path.
- If the file legitimately lives elsewhere in a monorepo, widen the configured project/dev root so the file is inside it.
- Inline the content as a string instruction instead of a FileSource if the file cannot be moved.
Example fix
// before
agent({ instructions: file("../shared/prompts/system.md"), /* ... */ });
// after
agent({ instructions: file("prompts/system.md"), /* ... */ }); // move the file under the project root Defensive patterns
Strategy: validation
Validate before calling
import { relative, isAbsolute } from "node:path";
function escapesRoot(root: string, source: string): boolean {
const rel = relative(root, source);
return rel === ".." || rel.startsWith("..") || isAbsolute(rel);
}
// check every FileSource path before dev/build:
for (const p of agentFileSourcePaths(definition)) {
if (escapesRoot(projectRoot, p)) throw new Error(`move ${p} inside ${projectRoot}`);
} Prevention
- Keep all prompt/context files under the project root; use root-relative paths.
- In monorepos, configure the dev root to the workspace root if sources span packages.
- Run a preflight check of FileSource paths when they come from user configuration.
When it happens
Trigger: A FileSource path (agent instructions or context source) pointing outside the root via '../', or an absolute path; or a module pulled into the project source graph that lives outside the root. The message tells you whether it came from the 'source graph' or a 'file source'.
Common situations: Instructions loaded from a shared monorepo package or a home-directory prompt file; contexts referencing ../../docs; workspaces where the agent root was configured as a subdirectory while imports reach a sibling package; passing an absolute path like /etc/... or /home/user/prompt.md.
Related errors
- caveman agent: dev ${kind} symlink escapes project root
- cave_harness_aborted
- cave_vercel_terminal_failure
- caveman agent: invalid .caveman/provider.json
- lock disappeared during validation
AI-assisted analysis of JuliusBrussee/caveman@27d5a3981a (2026-08-15).
Data as JSON: /api/errors/c505457db6544590.
Report an issue: GitHub.