JuliusBrussee/caveman · error
caveman build: source graph symlink escapes project root
Error message
caveman build: source graph symlink escapes project root
What it means
After lexically resolving an import inside the project, the walker takes its realpath and requires it to stay within the canonical project root. When the lexical path is inside the root but the realpath escapes it, a symlink is leading outside the project, which the build refuses with this dedicated message.
Solutions
- Replace the out-of-root symlink with a real copy of the target file inside the project
- If the target is shared source, bring it in as a workspace package / npm dependency instead of a symlink
- Adjust the traversal stop-roots configuration if the external location is intentional
- Remove or repoint stale symlinks created by an environment setup script
Example fix
// before: symlink escapes root ln -s ~/shared/helpers.ts src/helpers.ts // after: vendored copy inside root cp ~/shared/helpers.ts src/helpers.ts
Defensive patterns
Strategy: validation
Validate before calling
import { realpathSync } from 'fs';
const rp = realpathSync(fileInProject);
if (!rp.startsWith(canonicalRoot + '/')) throw new Error(`symlink escapes root: ${fileInProject}`); Try / catch
try {
await build({ files: sourceFiles(entry) });
} catch (e) {
if (e.message.includes('symlink escapes project root')) {
console.error('Replace out-of-root symlinks with vendored copies or workspace deps');
} else throw e;
} Prevention
- Avoid symlinking source files to locations outside the repo
- Use workspace packages for shared code instead of symlinks
- Add a prebuild script that rewrites or flags escaping symlinks
When it happens
Trigger: expandSourceGraph resolves an in-root relative import to a path that realpath() shows points outside canonicalRoot (symlink to a file elsewhere), while the symlink itself sits inside the project.
Common situations: Symlinking node_modules or src files to a shared location outside the repo; workspace links pointing at absolute paths outside the project root; copying the project with dangling/re-pointed symlinks.
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
- caveman build: imported source escapes project root
- caveman build: package artifact symlink is not lockable
- is a symlink; refusing transactional ownership mutation
- : database parent changed
- cave_sandbox_source_read_root_refused
AI-assisted analysis of JuliusBrussee/caveman@3ee70a1026 (2026-09-20).
Data as JSON: /api/errors/8c475ebf7b863f96.
Report an issue: GitHub.
Appendix: 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 3ee70a1026)