JuliusBrussee/caveman · error
caveman build: package artifact symlink is not lockable: ${J
Error message
caveman build: package artifact symlink is not lockable: ${JSON.stringify(path)} What it means
collectPackageFiles walks every directory of an installed package and throws when it encounters a symlink entry. The build lock must hash package artifacts deterministically; a symlink inside a package is not a stable, lockable artifact (its target can change independently of the package's own content), so the closure is rejected with the offending path in the message.
Source
Thrown at packages/agent/src/source-graph.ts:203
if (!required.has(dependency)) continue;
throw new Error(`caveman build: unresolved package dependency ${JSON.stringify(dependency)}`, {
cause: error,
});
}
}
}
async function collectPackageFiles(directory: string, files: Set<string>): Promise<void> {
const entries = await opendir(directory);
for await (const entry of entries) {
if (entry.name === "node_modules" || entry.name === ".git") continue;
const path = resolve(directory, entry.name);
if (entry.isDirectory()) {
await collectPackageFiles(path, files);
} else if (entry.isFile()) {
files.add(path);
} else if (entry.isSymbolicLink()) {
throw new Error(`caveman build: package artifact symlink is not lockable: ${JSON.stringify(path)}`);
}
}
}
function barePackageName(specifier: string): string {
const parts = specifier.split("/");
if (specifier.startsWith("@")) {
if (parts.length < 2 || parts[1] === "") throw new Error("invalid scoped package");
return `${parts[0]}/${parts[1]}`;
}
if (parts[0] === "") throw new Error("invalid package");
return parts[0]!;
}
function resolvePackageExport(exportsValue: unknown, subpath: string): string | undefined {
if (typeof exportsValue === "string" || Array.isArray(exportsValue)) {
return subpath === "." ? resolveConditionalExport(exportsValue) : undefined;
}View on GitHub (pinned to 27d5a3981a)
Solutions
- Reinstall the affected package cleanly so no postinstall or manual step recreates internal symlinks.
- Identify the path in the error message and replace the symlink with a real file or remove it if it is stray.
- If the package legitimately ships symlinks, report it to the maintainer or pin a version without them.
Example fix
# before ln -s ../prebuilt/bin.bin node_modules/pkg/bin.bin # manual patch # after # restore the real file from the package tarball: # rm node_modules/pkg/bin.bin && pnpm install --force
Defensive patterns
Strategy: validation
Validate before calling
import { readdir, lstat } from "node:fs/promises";
async function assertNoSymlinksInPackage(pkgDir: string): Promise<void> {
for (const entry of await readdir(pkgDir, { withFileTypes: true })) {
if (entry.name === "node_modules" || entry.name === ".git") continue;
const p = resolve(pkgDir, entry.name);
if (entry.isDirectory()) await assertNoSymlinksInPackage(p);
else if ((await lstat(p)).isSymbolicLink()) throw new Error(`symlink artifact: ${p}`);
}
} Try / catch
try {
await buildSourceGraph(root);
} catch (error) {
if (error instanceof Error && error.message.includes("package artifact symlink is not lockable")) {
// offending path is in the message; reinstall the package to restore real files
} else throw error;
} Prevention
- Never hand-patch installed packages with symlinks.
- Prefer clean reinstalls over in-place surgery on node_modules.
- Check packages that run postinstall scripts which create internal links.
When it happens
Trigger: Any symlink found while recursively walking a package directory under node_modules (excluding node_modules and .git entries themselves).
Common situations: Packages published with symlinks inside (bad packaging), postinstall scripts creating links inside package directories, or manual patching of installed packages with symlinks. Note the builder already resolves pnpm's top-level symlinked package roots to physical locations — this error is about symlinks within a package's own files.
Related errors
- caveman build: source graph symlink escapes project root
- caveman build: unresolved package dependency ${JSON.stringif
- caveman build: dataResidency is not enforced yet; refusing t
- caveman build: requiredFixturePassRate must be in (0,1]
- caveman build: qualityRetention must be in (0,1]
AI-assisted analysis of JuliusBrussee/caveman@27d5a3981a (2026-08-15).
Data as JSON: /api/errors/e0afd4fb6b8c3e73.
Report an issue: GitHub.