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

  1. Reinstall the affected package cleanly so no postinstall or manual step recreates internal symlinks.
  2. Identify the path in the error message and replace the symlink with a real file or remove it if it is stray.
  3. 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

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


AI-assisted analysis of JuliusBrussee/caveman@27d5a3981a (2026-08-15). Data as JSON: /api/errors/e0afd4fb6b8c3e73. Report an issue: GitHub.