paperclipai/paperclip · error · Error
Asset filename must match its SHA-256 digest
Error message
Asset filename must match its SHA-256 digest
What it means
The publish script enforces content-addressed naming: each asset filename must start with 'assets/<sha256-of-file-content>.' so the immutable CDN cache never serves stale bytes under a reused name. This error means the file's actual SHA-256 digest does not match the digest embedded in the filename declared in the manifest.
Solutions
- Recompute the SHA-256 of the asset file and rename it to assets/<new-digest>.<ext>
- Regenerate current.json so manifest.announcement[kind].path references the new digest filename
- Re-run your manifest-generation step after any asset change instead of hand-editing paths
- Verify with: sha256sum assets/<file> and compare to the filename
Example fix
// before assets/old-digest.png (bytes changed, name stale) // after d=$(sha256sum assets/image.png | cut -d' ' -f1) && mv assets/image.png "assets/$d.png"
Defensive patterns
Strategy: validation
Validate before calling
import { createHash } from "node:crypto";
const digest = createHash("sha256").update(await readFile(file)).digest("hex");
if (!assetPath.startsWith(`assets/${digest}.`)) throw new Error("rename asset to assets/<sha256>.<ext>"); Try / catch
try { await prepareAnnouncementPublish(src, staging, prefix); } catch (e) { if (e.message.includes("SHA-256 digest")) { /* recompute digest, rename file, regenerate manifest */ } } Prevention
- Regenerate digest filenames from final asset bytes, never before the last edit
- Automate manifest generation so paths always derive from computed hashes
- Never edit an asset in place after generating current.json
- Add a pre-publish sha256sum check comparing filenames to content digests
When it happens
Trigger: Editing or re-encoding an asset after computing its digest filename; renaming a file to another asset's digest; copying an asset and its manifest entry out of sync; generating the manifest before the final asset bytes were written.
Common situations: Re-running an image optimizer in place after current.json was generated; a build step that regenerates assets non-deterministically; hand-editing the manifest path; branching where the manifest and asset diverged.
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
- ACPX runtime executable digest mismatch
- ACPX private snapshot digest mismatch
- ACPX provider requires verified package snapshots
- ACPX snapshot manifest digest mismatch
- Animation must contain only visual HTML/CSS or inline SVG…
AI-assisted analysis of paperclipai/paperclip@3f1d897a7c (2026-09-18).
Data as JSON: /api/errors/6a7f6156eea5e830.
Report an issue: GitHub.
Appendix: source
Thrown at scripts/publish-announcements.ts:61
const source = path.resolve(sourceDirectory);
if (!(await lstat(source)).isDirectory()) throw new Error("Source must be a real directory");
const manifestPath = path.join(source, "current.json");
const stat = await lstat(manifestPath);
if (!stat.isFile() || stat.size > ANNOUNCEMENT_MANIFEST_MAX_BYTES) throw new Error("Invalid or oversized current.json");
const manifest = announcementManifestSchema.parse(JSON.parse(await readFile(manifestPath, "utf8")));
const files: Array<{ file: string; key: string; contentType: string; cacheControl: string }> = [];
for (const kind of ["image", "animation"] as const) {
const asset = manifest.announcement?.[kind];
if (!asset) continue;
if (!(await lstat(path.join(source, "assets"))).isDirectory()) throw new Error("Assets must be a real directory");
const assetPath = asset.path;
const file = path.join(source, assetPath);
const assetStat = await lstat(file);
const maximum = kind === "animation" ? ANNOUNCEMENT_ANIMATION_MAX_BYTES : ANNOUNCEMENT_IMAGE_MAX_BYTES;
if (!assetStat.isFile() || assetStat.size > maximum) throw new Error(`Invalid or oversized ${kind}`);
const bytes = await readFile(file);
const digest = createHash("sha256").update(bytes).digest("hex");
if (!assetPath.startsWith(`assets/${digest}.`)) throw new Error("Asset filename must match its SHA-256 digest");
if (kind === "animation") validateAnnouncementAnimation(bytes);
files.push({ file, key: `${prefix}/${assetPath}`, contentType: kind === "animation" ? "text/html" : assetPath.endsWith(".png") ? "image/png" : assetPath.endsWith(".jpg") ? "image/jpeg" : "image/webp", cacheControl: "public,max-age=31536000,immutable" });
}
files.push({ file: manifestPath, key: `${prefix}/current.json`, contentType: "application/json", cacheControl: "public,max-age=300" });
return { manifest, files };
}
export function announcementUploadArgs(bucket: string, file: Awaited<ReturnType<typeof prepareAnnouncementPublish>>["files"][number]) {
return ["s3api", "put-object", "--bucket", bucket, "--key", file.key, "--body", file.file,
"--content-type", file.contentType, "--cache-control", file.cacheControl];
}
async function main() {
const { sourceDirectory, staging, publish } = parseAnnouncementPublishArgs(process.argv.slice(2));
const hostPrefix = process.env.PAPERCLIP_PAGE_DEFAULT_PREFIX;
const prepared = await prepareAnnouncementPublish(sourceDirectory, staging, hostPrefix);
const bucket = process.env.PAPERCLIP_PAGE_BUCKET;
const baseUrl = process.env.PAPERCLIP_PAGE_BASE_URL?.replace(/\/+$/, "") ?? "https://pages.paperclip.ing";View on GitHub (pinned to 3f1d897a7c)