JuliusBrussee/caveman · error
caveman build: source graph symlink escapes project root
Error message
caveman build: source graph symlink escapes project root
What it means
A relative import resolved lexically to a file inside the project root, but its realpath (after following symlinks) lands outside the canonical root. The source graph locks files by physical identity; a symlink that escapes the project makes the closure non-finite/unverifiable from inside, so the build rejects it explicitly rather than silently following it out.
Source
Thrown at packages/agent/src/source-graph.ts:96
break;
} catch (error) {
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];View on GitHub (pinned to 27d5a3981a)
Solutions
- Remove the escaping symlink and either copy the file in or vendor it as a real dependency package installed through node_modules.
- Move the shared code inside the project root so the physical file is contained.
- If the target must stay outside, import it as a proper package dependency so it goes through the bare-specifier package closure path instead of a project-relative symlink.
Example fix
# before ln -s ~/shared/util.ts src/shared-util.ts # then import "./shared-util" # after # place the real file inside the project, or depend on it as a package:
Defensive patterns
Strategy: validation
Validate before calling
import { realpath, lstat } from "node:fs/promises";
import { relative, isAbsolute, resolve, dirname } from "node:path";
async function noEscapingSymlinks(projectRoot: string): Promise<void> {
const root = await realpath(projectRoot);
for (const file of await listProjectFiles(projectRoot)) {
const st = await lstat(file);
if (st.isSymbolicLink()) {
const real = await realpath(file);
const rel = relative(root, real);
if (isAbsolute(rel) || rel.startsWith("..")) throw new Error(`escaping symlink: ${file} -> ${real}`);
}
}
} Try / catch
try {
await buildSourceGraph(root);
} catch (error) {
if (error instanceof Error && error.message === "caveman build: source graph symlink escapes project root") {
// find the symlink (compare lexical vs realpath) and remove it or vendor the target
} else throw error;
} Prevention
- Never symlink shared source into the project; vendor it or depend on it as a package.
- Run a pre-build scan for symlinks inside the project directory.
- Remember the check is realpath-based: a link that looks inside can still resolve outside.
When it happens
Trigger: A symlink inside the project directory points to a file outside the project (e.g. linking a shared module from ~/common or another checkout), and a project source imports through it.
Common situations: Developers symlinking shared utilities between repos; monorepo tooling that links packages into src; editor plugins or patch-package-style workflows creating symlinks inside the source tree.
Related errors
- caveman-code: path escapes the workspace: ${candidate}
- caveman agent: dev ${kind} symlink escapes project root
- cave_tool_sandbox_entry_escapes_root
- cave_tool_sandbox_source_escapes_root
- caveman build: imported source escapes project root
AI-assisted analysis of JuliusBrussee/caveman@27d5a3981a (2026-08-15).
Data as JSON: /api/errors/8c475ebf7b863f96.
Report an issue: GitHub.