◉CLAUDE LABJP
●THREADS — Message threads arrived in beta on the Messages API (11/2): the server keeps the conversation and you send only new messages●CONSOLE — Console IP allowlist is in beta (11/5): requests from addresses not on the list are refused●SONNET 4.5 — Retires on the Claude API on 11/30, 25 days left. Migrate to Sonnet 5.5●WINDOWS — Reports say hooks break silently only on Japanese-locale Windows; check the character encoding first●FAST — Opus 4.8 fast mode is deprecated and will be removed on 11/30. Migrate to Opus 5.5●NEW — I Put a Ten-Line Promise File at the To…●THREADS — Message threads arrived in beta on the Messages API (11/2): the server keeps the conversation and you send only new messages●CONSOLE — Console IP allowlist is in beta (11/5): requests from addresses not on the list are refused●SONNET 4.5 — Retires on the Claude API on 11/30, 25 days left. Migrate to Sonnet 5.5●WINDOWS — Reports say hooks break silently only on Japanese-locale Windows; check the character encoding first●FAST — Opus 4.8 fast mode is deprecated and will be removed on 11/30. Migrate to Opus 5.5●NEW — I Put a Ten-Line Promise File at the To…
Articles/Claude Code
⟐ Claude Code/2026-08-08Advanced

Same Content, Different SHA-256: Making Pinned Plugin Archives Reproducible

Plugins can now be installed from an HTTPS zip with an optional SHA-256 pin. Building the same tree twice produced archives that differed in 50 bytes out of 22,669 and broke the pin every time. Here is the byte-level measurement and a packaging recipe that produced an identical hash 20 times out of 20.

Claude Code282plugins5SHA-256reproducible buildsdistribution

✦ Premium Article

I decided to package one of my own plugins for internal distribution the same day I learned that zip sources were supported. No git, no npm — one fixed location, contents verified by SHA-256. As an indie developer shipping to a handful of my own machines, that felt like exactly the right amount of machinery.

I recorded the hash of the archive I built, paused for a moment, then rebuilt it from the same directory. Not a single character had changed.

The hash was different.

The size was identical at 22,669 bytes. Extracting both archives produced identical files and identical structure. The SHA-256 still disagreed. Within ten minutes of adopting a pinning mechanism, I had broken it myself.

What follows is a byte-level account of where that difference lives, and how far I had to go to get twenty consecutive builds to produce the same hash. Every number here comes from an actual run in a Linux sandbox using Info-ZIP zip 3.0 and Python 3.

Where the 50 bytes live

The first thing to reproduce was a pair of independent checkouts. Git does not preserve mtimes. Every fresh clone stamps every file with the moment of checkout — which is precisely what a CI job doing clean checkouts produces.

# Two identical trees created two seconds apart, standing in for two clones
cp -r src co1 && sleep 2 && cp -r src co2
( cd co1 && zip -qr ../co1.zip . )
( cd co2 && zip -qr ../co2.zip . )
sha256sum co1.zip co2.zip
ArchiveSHA-256 (first 16)Size
co1.zipe550b24486bb82f922,669 bytes
co2.zipc35df325dd91365522,669 bytes

Counting the actual differences:

a = open('co1.zip', 'rb').read()
b = open('co2.zip', 'rb').read()
diff = [i for i, (x, y) in enumerate(zip(a, b)) if x != y]
print(len(diff), "/", len(a))          # -> 50 / 22669
 
runs = []
for i in diff:                          # collapse into contiguous ranges
    if runs and i == runs[-1][1] + 1:
        runs[-1][1] = i
    else:
        runs.append([i, i])
print(len(runs), sorted({r[1] - r[0] + 1 for r in runs}))   # -> 50 [1]

Fifty bytes out of 22,669 — 0.22% — and all fifty were isolated single bytes rather than contiguous blocks.

The extracted trees, meanwhile, were identical:

( cd x1 && find . -type f | LC_ALL=C sort | xargs sha256sum | sha256sum )
# 87f1d6cc4b71348a23ab7237f92d07fc7e1b04d3c993b5054ac67b75225df440
( cd x2 && find . -type f | LC_ALL=C sort | xargs sha256sum | sha256sum )
# 87f1d6cc4b71348a23ab7237f92d07fc7e1b04d3c993b5054ac67b75225df440

The culprit is timestamps. This archive held 11 entries (5 of them directories), and each entry carried the modification time in three separate places.

LocationFormatPer entry
Local file headerMS-DOS time and date4 bytes
Central directoryMS-DOS time and date4 bytes
UT extra field32-bit Unix time4 bytes, twice

UT is the extended timestamp Info-ZIP adds by default; in this tree the extra fields totaled 264 bytes. Because it stores whole seconds, it moves more readily than the DOS field. With only a two-second gap between builds, mostly low-order bytes shift — which is exactly why the diff came out as fifty scattered single bytes rather than a few solid runs.

The archive faithfully records when the files were read, not just what they contain, and the hash covers all of it. Obvious in hindsight. Not remotely obvious while you are filling in the pin.

Two consecutive builds can match by accident

This is the part I found genuinely dangerous.

The natural way to test reproducibility is to build twice in a row and compare. That is what I did. And that test passes.

MS-DOS timestamps have two-second resolution. Two builds that land in the same two-second window can share both the DOS field and the Unix seconds. Measuring across intervals:

Gap between tree creationsBuild 1 (first 12)Build 2Result
0 seconds034179b40720034179b40720match
1 secondfd37aad07691f83beb9efdb4differ
3 secondsbdc8402f83a5d978834c43c9differ

My expectation had been the opposite: if timestamps are embedded, surely the hash changes every single time. Instead, testing quickly makes the problem disappear. Local check green, CI red the next morning, and a reproduction condition that depends on wall-clock timing — a combination that tends to cost an afternoon.

The workaround is trivial. If you want to verify reproducibility, leave at least three seconds between builds, or deliberately scramble the mtimes as shown below — I recommend baking that scrambling into the check itself. Two back-to-back builds agreeing proves nothing.

✦

Thank you for reading this far.

Continue Reading

What follows includes implementation code, benchmarks, and practical content we hope you'll find useful. This site runs without ads — server and development costs are supported entirely by members like you. If it's been helpful, we'd be truly grateful for your support.

WHAT YOU'LL LEARN
✦A byte-level breakdown of why two independently built archives with identical contents differ in exactly 50 isolated bytes while the extracted tree hashes the same
✦MS-DOS timestamps have two-second granularity, so back-to-back builds can match by accident. Measured at 0, 1, and 3 second intervals to show how a reproducibility check silently passes
✦A pack.sh that normalizes mtimes, fixes entry order with LC_ALL=C, strips extra fields, and pins the compression level, producing the same hash 20 out of 20 times, plus a verify.sh for the consuming side
✦An audit script that reads only the central directory and judges repack risk from UT, ux, and entry order, plus a measured breakdown showing that of the four normalization steps only the mtime fix carries the determinism
Secure payment via Stripe · Cancel anytime
✦

Unlock This Article

Get full access to the rest of this article. Buy once, read anytime. This site is ad-free — your support goes directly toward keeping it running.

or
Unlock all articles with Membership →
Share

Thank You for Reading

Claude Lab is ad-free, supported entirely by members like you. We publish practical guides daily with implementation code, benchmarks, and production-ready patterns. If you've found it useful, we'd love to have you on board.

  • ✦Copy-paste ready implementation code
  • ✦New advanced guides published daily
  • ✦$5/mo or $10 for lifetime access
View Membership →

Related Articles

⟐ Claude Code2026-09-06
I counted 42 skills on my shelf, and four of them were just a note saying they had moved
Claude Code v2.1.261 added /skill-doctor. Before running it I counted my own skill shelf from the files, found four tombstones, and learned that the obvious awk one-liner for spotting duplicate plugin names quietly reports skills that are not duplicates at all.
⟐ Claude Code2026-11-06
When claude doctor Listed Four Warnings, I Sorted Them by Cause Before Fixing Anything
claude doctor ended with "4 warnings found", but I didn't need four fixes. Using the real output, here is how to spot duplicated warnings and sign-in-only info lines, group the rest by cause, and decide the order of repair.
⟐ Claude Code2026-11-06
Claude Code Shipped 21 Releases in Four Weeks. I Counted Before Deciding How Often to Update
Should you check Claude Code updates every day or once a week? I counted npm's publish history with a standard-library Python script and used the last 28 days of counts and gaps to set a rhythm.