paperclipai/paperclip · critical · Error
Artifact bytes do not match their immutable pin.
Error message
Artifact bytes do not match their immutable pin.
What it means
verifyBytes compares downloaded or on-disk artifact bytes against the manifest's immutable pin (exact size and sha512 integrity). This error means the bytes diverge from what the manifest pins — the file was corrupted, truncated, replaced, or the manifest describes a different build. It is the content-addressing integrity gate for the whole bundle.
Solutions
- Re-download or re-fetch the bundle fresh (rm the local artifacts and re-run the build or verify) so bytes and manifest come from the same run
- Rebuild the bundle with `node scripts/cloud-migrator-artifacts.mjs build <dir> <sha>` so manifest pins match current bytes
- Confirm the CDN/S3 object was not overwritten (uploads use --if-none-match * for immutability); if it was, treat the artifact as compromised and re-publish under the correct hash
- Check the directory contains files from a single build (manifest.json, package-lock.json, db.tgz, shared.tgz all same SHA)
Example fix
// before (file edited after manifest written) verifyBytes(bytes, pin) -> sha512(actual) != pin.integrity // after (rebuild so pins match bytes) node scripts/cloud-migrator-artifacts.mjs build ./bundle <sha> node scripts/cloud-migrator-artifacts.mjs validate ./bundle <sha>
Defensive patterns
Strategy: validation
Validate before calling
import { createHash } from "node:crypto";
const integrityFor = (b) => `sha512-${createHash("sha512").update(b).digest("base64")}`;
const bytes = readFileSync(path.join(dir, "db.tgz"));
const pin = manifest.packages.db;
if (bytes.length !== pin.size || integrityFor(bytes) !== pin.integrity) throw new Error("db.tgz does not match manifest pin"); Type guard
const matchesPin = (bytes, pin) => Buffer.isBuffer(bytes) && bytes.length === pin.size && integrityFor(bytes) === pin.integrity;
Try / catch
try {
validateBundle(dir, sha);
} catch (err) {
if (err.message === "Artifact bytes do not match their immutable pin.") {
// bytes and manifest disagree: rebuild or re-download from scratch
rmSync(path.join(dir, "db.tgz"));
buildBundle(dir, sha); // or re-run verifyPublished to re-download
} else throw err;
} Prevention
- Never modify files inside a built bundle; the manifest pins are immutable
- Discard and rebuild a bundle after any failed/partial write
- Trust only content-addressed URLs (hash-in-path) when downloading
- Keep all four bundle files (manifest, lock, two tgzs) from one build run
When it happens
Trigger: verifyBytes(bytes, pin) is called from validateBundle (local files) or verifyPublished (downloads) and either bytes.length !== pin.size or integrityFor(bytes) !== pin.integrity.
Common situations: A partially failed download or truncated tgz on disk; CDN serving a stale cached blob under an address that was overwritten; editing a tgz/json after the manifest was written; mixing files from two different builds of the same SHA.
Understand the failure class
Background: Checksum mismatch errors: "checksum verification failed", "digest mismatch", "expected vs actual checksum" — what they mean and how to fix them — this error's family across 41 libraries.
Related errors
- Materialized OpenCode executable digest mismatch
- runnerd digest mismatch: expected
- ACPX runtime executable digest mismatch
- ACPX private snapshot digest mismatch
- ACPX snapshot manifest digest mismatch
AI-assisted analysis of paperclipai/paperclip@3f1d897a7c (2026-09-18).
Data as JSON: /api/errors/e71757286aa1ca50.
Report an issue: GitHub.
Appendix: source
Thrown at scripts/cloud-migrator-artifacts.mjs:96
writeFileSync(path.join(scratch, "package.json"), JSON.stringify(root));
exec("npm", ["install", "--package-lock-only", "--ignore-scripts", "--no-audit", "--no-fund", "--registry=https://registry.npmjs.org"], { cwd: scratch, stdio: "inherit", timeout: 180_000 });
const lock = JSON.parse(readFileSync(path.join(scratch, "package-lock.json"), "utf8"));
// Both new packages are local during resolution. npm ci subsequently uses
// these immutable URLs, without looking up the new npm versions.
lock.packages[""].dependencies = { "@paperclipai/db": versionFor(sha) };
for (const name of names) lock.packages[`node_modules/@paperclipai/${name}`].resolved = packages[name].url;
const lockBytes = Buffer.from(JSON.stringify(lock) + "\n");
const manifest = { version: 1, sourceSha: sha, packageVersion: versionFor(sha), packages, lockfile: descriptor(lockBytes, "json") };
assertManifest(manifest, sha);
assertLockfile(lock, manifest);
writeFileSync(path.join(directory, "package-lock.json"), lockBytes);
writeFileSync(path.join(directory, "manifest.json"), JSON.stringify(manifest) + "\n");
return manifest;
} finally { rmSync(scratch, { recursive: true, force: true }); }
}
function verifyBytes(bytes, pin) {
if (bytes.length !== pin.size || integrityFor(bytes) !== pin.integrity) throw new Error("Artifact bytes do not match their immutable pin.");
}
export function validateBundle(directory, sha) {
const manifest = JSON.parse(readFileSync(path.join(directory, "manifest.json"), "utf8"));
assertManifest(manifest, sha);
for (const name of names) {
const bytes = readFileSync(path.join(directory, `${name}.tgz`));
verifyBytes(bytes, manifest.packages[name]);
assertMetadata(tarManifest(bytes), `@paperclipai/${name}`, sha);
}
const bytes = readFileSync(path.join(directory, "package-lock.json"));
verifyBytes(bytes, manifest.lockfile);
assertLockfile(JSON.parse(bytes), manifest);
return manifest;
}
/** Exercise the real dependency graph before publishing, with no new npm versions. */
export function verifyInstall(directory, sha, { exec = execFileSync } = {}) {View on GitHub (pinned to 3f1d897a7c)