CLAUDE LABJP
2.1.273 — A round of connection work landed together: five opt-in headers for LLM gateways, and a notice when Claude Code stops trying to reconnect an MCP server09/29 — The date beside claude-sonnet-4-5 is 12 days out, but it is an earliest-possible estimate. The model is still Active, and public retirements get at least 60 days noticeMCP — People keep asking to reconnect a dropped server without ending the session. The disconnect is now announced, but reattaching is still something you do by handNEW — A scheduled task ran some days and not others. The cause was that only one folder had been bound to itWINDOWS — When Cowork fails on its very first task, check developer mode and the setup state before going looking for a causeHANDOFF — Before a long draft gets too heavy for one chat, decide on the three things the summary must carry into the next one2.1.273 — A round of connection work landed together: five opt-in headers for LLM gateways, and a notice when Claude Code stops trying to reconnect an MCP server09/29 — The date beside claude-sonnet-4-5 is 12 days out, but it is an earliest-possible estimate. The model is still Active, and public retirements get at least 60 days noticeMCP — People keep asking to reconnect a dropped server without ending the session. The disconnect is now announced, but reattaching is still something you do by handNEW — A scheduled task ran some days and not others. The cause was that only one folder had been bound to itWINDOWS — When Cowork fails on its very first task, check developer mode and the setup state before going looking for a causeHANDOFF — Before a long draft gets too heavy for one chat, decide on the three things the summary must carry into the next one
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 Code253plugins4SHA-256reproducible buildsdistribution2

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 $15 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-03-22
Claude Code Channels: How to Control Your Coding Agent from Telegram and Discord
Claude Code Channels let you send messages from Telegram or Discord directly into a running session. Learn how to set up, configure, and use this powerful new feature for remote development.
Claude Code2026-03-20
Implementation Patterns for Custom Claude Code Skills — SKILL.md Design, Testing, and Distribution
A hands-on tutorial for building three production custom Claude Code skills from scratch. Covers SKILL.md structure, agent types, context injection, testing, and team distribution.
📚RECOMMENDED BOOKS
Build a Large Language Model (From Scratch)
Sebastian Raschka
LLM Dev
Prompt Engineering for LLMs
Berryman & Ziegler
Prompting
AI Engineering
Chip Huyen
AI Eng
* Contains affiliate links