JuliusBrussee/caveman · critical · Error
caveman agent: file source escapes project root
Error message
caveman agent: file source escapes project root
What it means
When building IR segments from a FileSource, the path is resolved against the project root and rejected if the resolved path escapes it (leading ../, backslash variant, or an absolute result). Like the workspace containment check in code.ts, this prevents segment construction from reading files outside the declared project.
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 27d5a3981a)
Solutions
- Use file paths relative to and inside the project root you passed in
- Move or copy the needed file into the project tree, or pass a rootDir that legitimately contains it
- Remove/avoid symlinks that cross the project boundary
- For string content, pass a plain string source instead of FileSource — strings bypass the path check
Example fix
// before
addSegment({ source: { path: "../../shared/config.json" }, rootDir: projectRoot });
// after
addSegment({ source: { path: "shared/config.json" }, rootDir: projectRoot }); // shared/ copied into project Defensive patterns
Strategy: type-guard
Validate before calling
import { resolve, relative, isAbsolute } from "node:path";
function insideRoot(rootDir: string, p: string): boolean {
const rel = relative(rootDir, resolve(rootDir, p));
return rel !== ".." && !rel.startsWith("../") && !rel.startsWith("..\\") && !isAbsolute(rel);
} Type guard
function isSafeRelativePath(p: string): boolean {
const norm = p.replace(/\\/g, "/");
return !norm.startsWith("/") && !/^[A-Za-z]:/.test(norm) && !norm.split("/").includes("..");
} Prevention
- Validate file-source paths against the project root before adding segments
- Prefer string sources for content that originates outside the project
- Configure rootDir to the true project root, not an arbitrary subdirectory
When it happens
Trigger: Passing a FileSource with path '../../../etc/hosts', an absolute path like '/etc/hosts', or an in-root symlink whose real target is outside the project directory.
Common situations: Config generators pointing at files in parent directories; monorepo setups where the project root was misconfigured to a subpackage; symlinked assets; templates with user-supplied paths.
Related errors
- caveman-code: path escapes the workspace: ${candidate}
- cave_live_eval_sandbox_profile_escapes_root
- cave_host_sandbox_nested_under_required
- cave_memory_tenant_invalid
- cave_memory_agent_invalid
AI-assisted analysis of JuliusBrussee/caveman@27d5a3981a (2026-08-15).
Data as JSON: /api/errors/034b9ef83aab4d25.
Report an issue: GitHub.