{"record":{"id":"e0afd4fb6b8c3e73","repo":"JuliusBrussee/caveman","slug":"caveman-build-package-artifact-symlink-is-not-loc","errorCode":null,"errorMessage":"caveman build: package artifact symlink is not lockable: ${JSON.stringify(path)}","messagePattern":"caveman build: package artifact symlink is not lockable: (.+?)","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"packages/agent/src/source-graph.ts","lineNumber":203,"sourceCode":"      if (!required.has(dependency)) continue;\n      throw new Error(`caveman build: unresolved package dependency ${JSON.stringify(dependency)}`, {\n        cause: error,\n      });\n    }\n  }\n}\n\nasync function collectPackageFiles(directory: string, files: Set<string>): Promise<void> {\n  const entries = await opendir(directory);\n  for await (const entry of entries) {\n    if (entry.name === \"node_modules\" || entry.name === \".git\") continue;\n    const path = resolve(directory, entry.name);\n    if (entry.isDirectory()) {\n      await collectPackageFiles(path, files);\n    } else if (entry.isFile()) {\n      files.add(path);\n    } else if (entry.isSymbolicLink()) {\n      throw new Error(`caveman build: package artifact symlink is not lockable: ${JSON.stringify(path)}`);\n    }\n  }\n}\n\nfunction barePackageName(specifier: string): string {\n  const parts = specifier.split(\"/\");\n  if (specifier.startsWith(\"@\")) {\n    if (parts.length < 2 || parts[1] === \"\") throw new Error(\"invalid scoped package\");\n    return `${parts[0]}/${parts[1]}`;\n  }\n  if (parts[0] === \"\") throw new Error(\"invalid package\");\n  return parts[0]!;\n}\n\nfunction resolvePackageExport(exportsValue: unknown, subpath: string): string | undefined {\n  if (typeof exportsValue === \"string\" || Array.isArray(exportsValue)) {\n    return subpath === \".\" ? resolveConditionalExport(exportsValue) : undefined;\n  }","sourceCodeStart":185,"sourceCodeEnd":221,"githubUrl":"https://github.com/JuliusBrussee/caveman/blob/27d5a3981a347890211bb1bf2439e5c821a63bc9/packages/agent/src/source-graph.ts#L185-L221","documentation":"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.","triggerScenarios":"Any symlink found while recursively walking a package directory under node_modules (excluding node_modules and .git entries themselves).","commonSituations":"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.","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."],"exampleFix":"# before\nln -s ../prebuilt/bin.bin node_modules/pkg/bin.bin # manual patch\n\n# after\n# restore the real file from the package tarball:\n# rm node_modules/pkg/bin.bin && pnpm install --force","handlingStrategy":"validation","validationCode":"import { readdir, lstat } from \"node:fs/promises\";\nasync function assertNoSymlinksInPackage(pkgDir: string): Promise<void> {\n  for (const entry of await readdir(pkgDir, { withFileTypes: true })) {\n    if (entry.name === \"node_modules\" || entry.name === \".git\") continue;\n    const p = resolve(pkgDir, entry.name);\n    if (entry.isDirectory()) await assertNoSymlinksInPackage(p);\n    else if ((await lstat(p)).isSymbolicLink()) throw new Error(`symlink artifact: ${p}`);\n  }\n}","typeGuard":null,"tryCatchPattern":"try {\n  await buildSourceGraph(root);\n} catch (error) {\n  if (error instanceof Error && error.message.includes(\"package artifact symlink is not lockable\")) {\n    // offending path is in the message; reinstall the package to restore real files\n  } else throw error;\n}","preventionTips":["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."],"tags":["build","symlink","node-modules","package-closure"],"backgroundTag":null,"analyzedSha":"27d5a3981a347890211bb1bf2439e5c821a63bc9","analyzedAt":"2026-08-15T09:26:11.751Z","schemaVersion":2},"datasetVersion":"2026-08-15T17:31:12.345Z"}