Post-Mortem ⏱️ 7 min read

We Shipped Five Skills Nobody Could Install

TL;DR

💥 The symptom: Every paid Godmode skill installed through our MCP server, reported success, then quietly vanished. Claude Code couldn't see it.

📦 Bug one (visible): The zip wrapped the skill in an extra folder, so it unpacked to skills/<slug>/<slug>/ — one level too deep to register.

🔑 Bug two (silent): Before extracting, the installer checks a sha256 fingerprint. The fingerprint pinned on the server had drifted from the actual bytes, so the download hard-failed.

🔧 The fix: Rebuild every zip flat, re-pin the fingerprints to the exact bytes, and ship all three moving parts in one atomic deploy.

The result: All five skills install clean again. Verified end-to-end through the MCP.

🔌 The symptom: install succeeds, skill disappears

You ask Claude to install a Godmode skill. The MCP server says "installed." You restart Claude Code and the skill isn't there.

No error. No stack trace. The single most useless kind of failure: the one that reports success.

Installing a skill is a six-step relay. Each step hands off to the next, and two of those steps were quietly broken. Click through the chain to see where.

[1] godmode_install_skill
You name a skill by slug. The MCP server is the front door: it takes the request and asks the backend whether you own this skill.
This step was fine. The request always reached the server.

The two broken links were the last two. Verify-the-fingerprint and unpack-to-disk. Everything up to that point worked, which is exactly why "installed" came back before anything actually landed.

🔏 What a sha256 fingerprint actually is

A sha256 is a fingerprint of a file. Feed in the exact bytes, get back the same 64-character code every time. Change one byte and the code changes completely.

The installer uses it as a bouncer. It downloads the zip, recomputes the fingerprint, and compares it to a value the server promised in advance. Match, and it unpacks. No match, and it refuses.

same bytes
→ same code

one byte off
→ total mismatch
ZIP BYTES 50 4b 03 04 14 00 08 00 08 00 ... sha 256 FINGERPRINT (64 HEX) b1eb16d8bf0bb2d8 5a12c1bceafe46b1 Recompute after download · compare to the promised code [ match ] unpack the skill [ no match ] refuse — the file is not what was promised

Think of it like a tamper-evident seal on a parcel. The courier checks the seal against the number on the docket before handing it over. If they don't match, the parcel is either damaged or swapped, and it doesn't get delivered. The fingerprint is that seal for a download: it proves the bytes are exactly the ones we meant to send, not a corrupted copy or a wrong file.

So the check is a feature, not the bug. It's the thing standing between you and a half-downloaded or tampered skill. The bug was that we let the promised fingerprint drift away from the actual bytes — so the bouncer started turning away the real guest.

📦 Bug one: the zip nested one folder too deep

A skill is a folder with a SKILL.md at its root. Claude Code looks for it at exactly skills/<slug>/SKILL.md. One level off and the skill is invisible.

Our packaging script zipped each skill with its own folder inside. On extraction that nests an extra level. Toggle between the two and watch the path.

The wrapper version put SKILL.md at skills/godmode/godmode/SKILL.md. Claude Code looked in skills/godmode/, found a folder instead of a manifest, and registered nothing.

The fix was to repackage every skill flatSKILL.md and the runner files sitting at the zip root, no wrapper directory. One line in the build script. The whole class of bug disappears.

🔑 Bug two: the fingerprint had drifted

The expected fingerprint isn't computed live. It's pinned in a hardcoded catalog inside the server's edge function. When we rebuilt the zips to fix bug one, the bytes changed — but the pinned fingerprint still described the old bytes.

So the installer downloaded the new, correct zip, recomputed its fingerprint, compared it to the stale pin, and hard-failed. Doing its job perfectly, against the wrong reference. Pick a scenario and watch the gate decide.

bytes downloaded
new flat zip
recomputed sha256
b1eb16d8…
pinned in catalog
cdfe0bad…
[ FAIL ] · recomputed ≠ pinned · install refused

The fix was to re-pin the catalog to the fingerprints of the exact bytes we'd just built, then prove zero drift with a verifier script before anything shipped.

$ node scripts/verify-catalog-shas.mjs
  godmode-lite   2.3.0  b1eb16d8...  OK
  godmode        2.3.1  21635f56...  OK
  godmode-plus   2.7.0  a0bf2740...  OK
  evolution      3.3.0  2bf4e80b...  OK
  one-shot       3.12.0 dc525a10...  OK
  -> 0 drift. safe to deploy.

🚀 The fix has to ship in one piece

Here's the part that makes this more than a one-line patch. The fingerprint lives in three places that all have to agree, and they live in three different systems.

The bytes

The actual zip files, stored in Cloudflare KV. This is what the installer downloads.

The pin

The expected fingerprint, hardcoded in the catalog inside the Supabase edge function.

The gate

The downloads worker that serves a signed URL only for the filename the edge function expects.

Push the new bytes to KV but leave the old pin in place, and you flip every live install into a hard fingerprint failure. Deploy the new worker without the matching edge function, and a skill served under a renamed file stops resolving. Half a deploy is worse than no deploy.

So the release runs as one atomic script. All three systems, one pass, verified at the end.

one script
three systems
verified last
[1] Build flat — repackage all 5 skills, no wrapper dir

[2] Re-pin — copy the new fingerprints into the catalog

[3] Verifyverify-catalog-shas.mjs must report zero drift

[4] KV upload — push the 4 paid zips to Cloudflare KV

[5] Deploy edge fn — ship the catalog with --no-verify-jwt

[6] Deploy worker — the downloads gate

[7] Verify live — re-download, re-hash, confirm the bytes match
The atomic release. Steps 4, 5 and 6 must land together or live installs break.

Do

Treat the bytes, the pin, and the gate as one release. Rebuild, re-pin, verify zero drift, then deploy all three in a single pass and re-test a real download at the end.

Don't

Don't push zips to storage and "fix the catalog later." The moment the bytes and the pin disagree, every install in the wild hard-fails the fingerprint check.

📊 What shipped

Receipt, not a brag. Every paid skill rebuilt flat, every fingerprint re-pinned, one deploy script to make the three systems move together.

SkillVersionRepackagedInstall
Godmode Lite (free)2.3.0flat✅ clean
Godmode2.3.1flat✅ clean
Godmode+2.7.0flat✅ clean
Evolution3.3.0flat✅ clean
One-Shot Scripts3.12.0flat✅ clean
ChangeDetail
Root cause 1Wrapper-dir zips nested skills to skills/<slug>/<slug>/
Root cause 2Catalog sha256 pins drifted from the rebuilt bytes
Build changeAll zips repackaged flat (SKILL.md + runner at root)
New scriptdeploy-skill-release.sh — atomic KV + edge fn + worker
Guard addedverify-catalog-shas.mjs blocks any deploy with drift
VerificationEnd-to-end install of all 5 skills through the live MCP

🕵️ Why this was so sneaky

Success that lied

The MCP reported "installed" before the bytes ever landed on disk. A green checkmark over a failed extraction.

Drift you can't grep

The bytes and the pin live in two systems. Nothing in a code diff shows they've fallen out of sync.

Looks fine locally

Unzip the wrapper zip by hand and it looks perfectly normal. The nesting only bites the installer's exact path.

Three-part deploys

Storage, edge function, and worker deploy separately. Any one of them lagging half-breaks the release.

The lesson is not "fingerprint checks are fragile." The check did exactly what it should: it refused a file that didn't match its promise. The real discipline is keeping the promise and the bytes in lockstep, and never shipping one of them without the other.

Install Godmode from inside Claude

The skills are live and verified. Add the Godmode MCP server, and Claude can install any skill you own in one line — fingerprint-checked on the way in.

Get Godmode More post-mortems