JuliusBrussee/caveman · error
caveman build: imported source escapes project root
Error message
caveman build: imported source escapes project root
What it means
A relative import's resolved file lies outside the project root by both lexical path and realpath — the import genuinely reaches beyond the project (e.g. "../../.."). The builder's project closure must stay inside the canonical root; importing out of it is not lockable through the project path, so the build aborts with this error.
Source
Thrown at packages/agent/src/source-graph.ts:98
if ((error as NodeJS.ErrnoException).code !== "ENOENT") throw error;
}
}
if (!found) {
throw new Error(`caveman build: unresolved relative source import ${JSON.stringify(specifier)}`);
}
if (bare) {
await collectPackageClosure(packageRoot!, files, packageRoots);
continue;
}
const lexicalFound = found;
found = await realpath(found);
const insideRoot = isPathWithin(canonicalRoot, found);
if (!insideRoot && !bare && !external.has(path)) {
const lexicalInsideRoot = isPathWithin(canonicalRoot, lexicalFound);
if (lexicalInsideRoot) {
throw new Error("caveman build: source graph symlink escapes project root");
}
throw new Error("caveman build: imported source escapes project root");
}
if (!insideRoot) external.add(found);
if (!files.has(found)) {
files.add(found);
if (!traversalStopRoots.some((stop) => isPathWithin(stop, found))) {
queue.push(found);
}
}
}
}
return files;
}
function explicitImportCandidates(base: string, importer: string): string[] {
const importerExtension = extname(importer);
if (![".ts", ".tsx", ".mts", ".cts"].includes(importerExtension)) return [base];
const extension = extname(base);
const stem = base.slice(0, -extension.length);View on GitHub (pinned to 27d5a3981a)
Solutions
- Rewrite the import as a workspace/package dependency (bare specifier) declared in package.json so it resolves through the package closure path.
- Move the target file inside the project root and use a contained relative path.
- Fix stale ../ chains after restructuring — run tsc to list imports that no longer resolve inside the project.
Example fix
// before: project/a/b/src.ts
import { helper } from "../../../outside/helper.ts";
// after
import { helper } from "my-workspace-helper"; // declared dependency Defensive patterns
Strategy: validation
Validate before calling
import { resolve, relative, isAbsolute } from "node:path";
function importStaysInRoot(root: string, importer: string, spec: string): boolean {
if (!spec.startsWith(".")) return true; // bare: package path
const target = resolve(dirname(importer), spec);
const rel = relative(root, target);
return !isAbsolute(rel) && !rel.startsWith("..");
} Try / catch
try {
await buildSourceGraph(root);
} catch (error) {
if (error instanceof Error && error.message === "caveman build: imported source escapes project root") {
// convert the ../.. import into a declared package dependency or move the file inside
} else throw error;
} Prevention
- Forbid ../ chains that climb past the project root in lint rules (e.g. eslint import/no-relative-parent-imports).
- Use package dependencies for cross-package code instead of filesystem reach-arounds.
- Rewrite imports immediately after moving files between projects.
When it happens
Trigger: A source file uses ../ or ../../ specifiers that climb past the project root directory, or a moved file kept its old deep-relative import which now escapes.
Common situations: Files moved into the project from a sibling directory without rewriting imports; projects nested inside a workspace where sources reach at workspace siblings instead of using package imports.
Related errors
- caveman build: unresolved relative source import ${JSON.stri
- caveman build: source graph symlink escapes project root
- invalid package
- caveman build: dataResidency is not enforced yet; refusing t
- caveman build: requiredFixturePassRate must be in (0,1]
AI-assisted analysis of JuliusBrussee/caveman@27d5a3981a (2026-08-15).
Data as JSON: /api/errors/50b6b0bf6bc96b1d.
Report an issue: GitHub.