CLAUDE LABJP
RUNNER — claude self-hosted-runner turns your own machines or containers into a place web, mobile, and desktop sessions can run, on Team and Enterprise plansPLUGIN — Plugins can now be installed from a zip over HTTPS, with no git or npm required and optional SHA-256 pinningBEDROCK — ANTHROPIC_BEDROCK_REGION_PREFIX lets Bedrock prefer a specific cross-region inference profile instead of the one derived from AWS_REGIONSTATUS — Sessions waiting on a sandbox, MCP input, or managed-settings prompt now read as Needs input rather than WorkingDLP — Inference Hooks are in beta for Enterprise. Prompts route to your own security server for an allow or deny verdict, typically within five seconds, before the model sees themWORKBENCH — The legacy Workbench and experimental prompt tools API retire on August 17, and Sonnet 5 promotional pricing runs through August 31RUNNER — claude self-hosted-runner turns your own machines or containers into a place web, mobile, and desktop sessions can run, on Team and Enterprise plansPLUGIN — Plugins can now be installed from a zip over HTTPS, with no git or npm required and optional SHA-256 pinningBEDROCK — ANTHROPIC_BEDROCK_REGION_PREFIX lets Bedrock prefer a specific cross-region inference profile instead of the one derived from AWS_REGIONSTATUS — Sessions waiting on a sandbox, MCP input, or managed-settings prompt now read as Needs input rather than WorkingDLP — Inference Hooks are in beta for Enterprise. Prompts route to your own security server for an allow or deny verdict, typically within five seconds, before the model sees themWORKBENCH — The legacy Workbench and experimental prompt tools API retire on August 17, and Sonnet 5 promotional pricing runs through August 31
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 Code213plugins3SHA-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
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-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.
Claude Code2026-08-07
The Write Your Permission Mode Never Stops — Auditing an Agent-Scoped PreToolUse Gate Across 150 Payloads
Workflow subagents auto-approve file edits regardless of your session permission mode. I measured the remaining boundary — an agent_id-scoped PreToolUse hook — across 150 payloads, found 54 silent pass-throughs, and rebuilt it to fail closed.
📚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
See all →