JuliusBrussee/caveman · error
caveman agent: file source escapes project root
Error message
caveman agent: file source escapes project root
What it means
Thrown by sourceBytes in packages/agent/src/context-ir.ts when a FileSource resolves to a path outside the configured project rootDir. The library refuses to read any file whose relative path from root escapes upward (e.g. '../secrets'), preventing path traversal during context lowering. This is a deliberate security boundary, not a bug.
Solutions
- Rewrite the FileSource path so it stays inside rootDir (copy the file into the project or use a path relative to the root).
- Resolve the offending path manually and confirm it is inside rootDir before passing it in.
- If the asset legitimately lives outside the root, pass a rootDir that is the common ancestor directory of all sources.
- Check for symlinks pointing outside the project and remove or replace them.
Example fix
// before
await lowerContext({ sources: [{ path: "../shared/prompt.md" }], rootDir: projectRoot });
// after
await lowerContext({ sources: [{ path: "shared/prompt.md" }], rootDir: projectRoot }); // file copied into projectRoot/shared/ Defensive patterns
Strategy: validation
Validate before calling
import { resolve, relative, isAbsolute } from "node:path";
function isInsideRoot(rootDir: string, path: string): boolean {
const rel = relative(rootDir, resolve(rootDir, path));
return !(rel === ".." || rel.startsWith("../") || rel.startsWith("..\\") || isAbsolute(rel));
}
if (!isInsideRoot(rootDir, source.path)) throw new Error(`source path escapes root: ${source.path}`); Try / catch
try {
await lowerContext({ sources, rootDir });
} catch (e) {
if (e instanceof Error && e.message.includes("escapes project root")) {
console.error(`Invalid source path: rewrite it relative to ${rootDir}`, e.message);
} else throw e;
} Prevention
- Always express FileSource paths relative to rootDir; never concatenate user input into them.
- Run the relative()/startsWith('..') check on every dynamically built path before calling the API.
- Watch for symlinks — resolve real paths if sources may link outside the project.
- Copy external assets into the project tree instead of referencing parent directories.
When it happens
Trigger: Calling lowerContext with a FileSource whose path contains '..' segments or an absolute path that resolves outside rootDir; e.g. { path: '../../etc/passwd' } or { path: '/etc/passwd' } while rootDir is the project directory.
Common situations: Symlinks resolving outside the root, user-supplied include paths not sanitized, config files referencing shared assets in a parent directory, or joining paths from untrusted plugin input.
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 escapes project root
- cave_live_eval_sandbox_profile_escapes_root
- cave_sandbox_source_read_grant_escapes_staging
- cave_sandbox_source_read_root_refused
AI-assisted analysis of JuliusBrussee/caveman@3ee70a1026 (2026-09-20).
Data as JSON: /api/errors/034b9ef83aab4d25.
Report an issue: GitHub.
Appendix: source
Thrown at packages/agent/src/context-ir.ts:325
lowered.ir.segments.push(appended);
return appended;
}
export function contextBill(ir: ContextIR): Record<string, number> {
const bill: Record<string, number> = {};
for (const segment of ir.segments) {
bill[segment.kind] = (bill[segment.kind] ?? 0) + segment.tokenCount;
}
return bill;
}
async function sourceBytes(source: string | FileSource, rootDir: string): Promise<Uint8Array> {
if (typeof source === "string") return new TextEncoder().encode(source);
const fullPath = resolve(rootDir, source.path);
const relativePath = relative(rootDir, fullPath);
if (relativePath === ".." || relativePath.startsWith("../") ||
relativePath.startsWith("..\\") || isAbsolute(relativePath)) {
throw new Error("caveman agent: file source escapes project root");
}
return new Uint8Array(await readFile(fullPath));
}
function encodeCanonical(value: unknown): Uint8Array {
return new TextEncoder().encode(stableStringify(value));
}
export function stableStringify(value: unknown): string {
if (value === null || typeof value !== "object") {
const encoded = JSON.stringify(value);
if (encoded === undefined) throw new Error("caveman agent: value is not canonically serializable");
return encoded;
}
if (Array.isArray(value)) return `[${value.map(stableStringify).join(",")}]`;
const object = value as Record<string, unknown>;
return `{${Object.keys(object).sort().map((key) => `${JSON.stringify(key)}:${stableStringify(object[key])}`).join(",")}}`;
}View on GitHub (pinned to 3ee70a1026)