This is an extra for Ship the Proof, alongside the ten-part series, because it applies the same argument to a surface the series has not covered yet: a Claude Code plugin marketplace. A plugin is code your assistant loads and runs with your credentials. If you install one from a catalog, you are trusting whoever last touched that catalog entry, and trusting it every time the assistant reads it, not just the day you installed it.

The modeled-information-format org runs two of these catalogs, claude-code-plugins and gdlc, and treats them with the same rigor the rest of this series applies to container images and source releases: every external plugin is pinned to a full SHA, every release is signed and attested keyless, and a fail-closed gate re-verifies before anything merges. Here is how that pipeline actually works, using the org’s real marketplace.json entries and real verification commands, and here is the claim I want to be specific about: this pipeline raises the cost of a supply-chain attack on these plugins. It does not remove the need for you to decide, yourself, when a pinned reference is worth upgrading.

The problem a marketplace catalog has that Dependabot does not solve

A .claude-plugin/marketplace.json lists plugin entries, and an external entry points at another repository’s content: a github source, a url, or a git-subdir path, each carrying a ref and, in a properly governed catalog, a sha. That sha is the whole guarantee. Point the entry at a mutable ref instead, a branch or a tag that can move, and the content Claude Code fetches today can differ from the content a reviewer approved yesterday. Nobody signed off on the difference, because nobody re-reviewed it. That is the generic rug-pull, and it needs no compromised credential to happen: force-push a tag, and every consumer pinned to that tag now runs whatever you pushed.

No existing tool closes this gap for you automatically. Dependabot cannot parse the git-subdir plus 40-character sha scheme these catalogs use, so it never proposes an update. Git submodule tracking follows branch HEAD, the exact mutable-ref problem this scheme exists to avoid. Someone has to bring a pinned reference current, and prove the new target is worth trusting, or the pin rots until a human remembers to touch it by hand.

The org’s answer is three separate mechanisms, each with a different failure posture, plus a signing and attestation pipeline underneath all three. None of them originated as a one-off for these two repos. They are documented instances of the org’s attested-delivery skill, a portable architecture for signed, SLSA-attested, fail-closed-verified releases that any repo in the org can adopt by calling the same reusable workflows. A hard-fail catalog-check gate keeps the skill’s workflow-catalog.md honest against what actually exists on disk, so the documentation cannot silently drift from the mechanism. If you are setting this up in your own org, that skill is the starting point. Do not hand-roll the signing and verification plumbing described below; it already exists as a reusable component.

The attested marketplace pipeline: the catalog-update hub verifies fail-closed before proposing a re-pin, catalog-admission re-verifies and merges, the auto-tag seam fires the tag-gated release, and a consumer independently re-verifies before trusting any of it

Mechanism one: verify before you propose

The plugin-catalog-update-hub workflow runs weekly and on demand. It enumerates every repository the org’s catalog GitHub App can reach, drops anything on a deny-list, and keeps only the repos carrying a marketplace.json. For each external plugin entry it finds, it does the same four things, in order: resolve the source repo’s latest release, dereference that release’s tag to a commit SHA, verify the release’s attestations fail-closed with gh attestation verify, and only then re-pin source.sha and source.ref and open an auto-merge pull request carrying the attestation evidence in its body.

Read that order again, because the order is the whole design. A conventional updater proposes first and lets review catch problems. This hub inverts it: nothing gets proposed unless it already verified. A release whose provenance does not check out is skipped and logged, never re-pinned. By the time a maintainer sees the PR, the thing being proposed has already survived the check that matters.

The hub does not merge anything itself. Its App token is scoped to one target repository at a time, and the merge decision belongs entirely to that repository’s own catalog-admission gate, which re-verifies the same attestations on the PR before allowing it through. That split matters for the residual-risk question later in this piece: the hub’s credential can open a pull request, but it cannot force one to land.

Mechanism two: the structural gate that never trusts a human to remember

Separately from the hub, a reusable manifest-review workflow checks every marketplace catalog and every plugin manifest against the structural invariants the whole system depends on. It runs as pure standard-library Python, no third-party action, no allow-list entry to maintain, which keeps the gate itself auditable rather than one more dependency to trust. It is soft-fail by design: findings normalize to SARIF and land in code scanning rather than blocking the pull request outright, because enforcement is a branch-protection decision layered on top, not baked into the gate.

The rule that matters most here is MR006: every external plugin source, whatever its type, must be pinned to a full 40-character SHA. Here is what a compliant entry actually looks like, taken from the claude-code-plugins marketplace as it exists today:

{
  "name": "mif-docs",
  "source": {
    "source": "github",
    "repo": "modeled-information-format/mif-docs-plugin",
    "ref": "v0.1.2",
    "sha": "0f4d0b4b31ef147952b9a243f2cca8cff1ff3c6c"
  },
  "description": "MIF-first documentation skill suite: one skill per document genre (Diataxis, ADR, RFC/PEP, runbook, PRD, Kiro, arc42/C4, changelog) over a MIF L1->L3 floor.",
  "author": { "name": "modeled-information-format" },
  "category": "documentation",
  "license": "MIT"
}

Side by side: a marketplace entry pinned only to the ref v0.1.2 follows whatever commit that tag is re-pushed to, while the same entry with the 40-character sha 0f4d0b4b31ef147952b9a243f2cca8cff1ff3c6c always resolves to that one commit regardless of what the tag later points to

The ref field, v0.1.2, is there for a human to read. The sha field is what actually resolves. If someone deletes and re-tags v0.1.2 on the upstream repo tomorrow, this entry keeps installing the exact commit 0f4d0b4b31ef147952b9a243f2cca8cff1ff3c6c, not whatever now answers to that tag. MR006 exists because a marketplace entry with a ref and no sha, or a sha shorter than 40 characters, is exactly as vulnerable to a tag rewrite as the mutable-ref problem this whole system was built to close.

Two more checks run in the same pass: MR003 rejects a marketplace name that collides with an Anthropic-reserved namespace, and a set of required-field rules make sure marketplace.json and every plugin.json carry what the marketplace depends on. None of these three mechanisms is optional cover for the others. The hub proposes verified updates, manifest-review catches structural drift on every contribution, and catalog-check keeps the reusable-workflow documentation from lying about what workflows exist. Three fail postures, one governed catalog.

The signing pipeline underneath all of it

Every release these mechanisms depend on, source archive or catalog blob, is signed keyless through Sigstore inside centralized reusable workflows the calling repository cannot modify. That last clause is the entire point: if the application repo could edit its own signing step, the signer identity would collapse to the caller, and the whole guarantee would be “I attest that I am who I say I am,” which attests nothing. Running the signing step in a workflow the caller cannot touch is what SLSA calls Build Level 3 isolation, and it is why the Fulcio certificate identity on every one of these signatures resolves to a workflow under modeled-information-format/.github, never to the application repo itself.

Source archives get SLSA build provenance and a CycloneDX SBOM bound to the tarball’s digest via actions/attest-build-provenance and actions/attest-sbom. The marketplace catalog blob, not a registry package, gets cosign sign-blob instead, because it has no digest to attach an OCI referrer to. Anyone can independently check either one, and the commands are not secret:

# Verify a source-archive release's SLSA provenance
gh attestation verify modeled-information-format-1.0.0.tar.gz \
  --owner modeled-information-format \
  --predicate-type https://slsa.dev/provenance/v1

# Verify the signed marketplace catalog blob, pinned to the signer workflow
cosign verify-blob .claude-plugin/marketplace.json \
  --bundle marketplace.json.cosign.bundle \
  --certificate-identity-regexp \
  '^https://github.com/modeled-information-format/\.github/\.github/workflows/reusable-cosign-sign\.yml@' \
  --certificate-oidc-issuer "https://token.actions.githubusercontent.com"

Notice the --certificate-identity-regexp in the second command. It does not just ask “is this signed.” It asks “is this signed by the one workflow authorized to sign it.” A valid Sigstore signature from the wrong workflow fails this check on purpose. That is the difference between a signature and a pinned signature, and it is what keeps a compromised or copy-pasted workflow elsewhere in the org from producing something that verifies as if it came from the real signer.

Nothing here runs on faith. Before any of these mechanisms publish or promote an artifact, they re-run their own verification in the same job, fail-closed. An empty required-predicate list is itself a failure, not a pass by default. The design assumes the pipeline’s own output is guilty until it proves itself, every single run.

Closing the freshness gap

There is one more piece worth naming because it closes a gap the first two mechanisms leave open. An admitted catalog change lands on the marketplace’s main branch the moment its pull request merges, but the marketplace’s own attested-release pipeline only fires on a human-pushed version tag. Left alone, those two facts mean the catalog on main and the catalog in the last signed release drift apart, and the consumer with the most disciplined verification habits, the one who only ever installs from a signed release rather than trusting branch HEAD, is the one stuck on the stalest copy.

A reusable auto-tag seam closes that gap. It listens on pushes to main, path-filtered to the catalog file only, computes the next patch version from the repository’s latest tag, and pushes that tag using the org’s release App identity rather than the run’s own ephemeral token, because a ref pushed with the run’s own token does not trigger downstream workflows. The pushed tag then fires the existing tag-gated release pipeline unchanged. Nothing about the release pipeline itself had to change: the seam supplies a trigger, not new logic, and it is structurally unable to trigger itself, since it listens on branch pushes and only ever writes a tag. Minor and major version bumps stay a human decision, made through a manual dispatch input; the automation only ever bumps the patch version, and a human-pushed tag on the same commit takes precedence over the automatic one.

The result: the staleness window between an admitted catalog change and its signed, attested release shrinks from “however long until someone remembers to tag” to “one pipeline run.” A consumer who insists on installing from a verified, signed release rather than branch HEAD is no longer the one penalized for having the stricter habit.

What this pipeline actually buys you

Stack all of it up and be precise about what it does. A rug-pull on a mutable ref stops working, because every consuming entry is pinned to a SHA that a re-tag cannot move. A tampered or misattributed signature stops verifying, because the check pins the exact signer workflow, not just “a valid signature from somewhere.” A repo cannot forge its own provenance, because the signing step runs in a workflow it cannot edit. An unverified release cannot silently reach a consumer, because the hub checks fail-closed before it ever proposes anything, and the target’s own admission gate re-checks before merge. Put together, this is a real, measurable increase in the cost of attacking one of these plugins. None of it is theater.

Now be equally precise about what it is not. It is not proof the code is safe, and that is not because these gates are decorative. Most of them are real merge gates: a verified live secret (Gitleaks, TruffleHog), a Trivy image vulnerability, and the dependency-review check fail the job outright, and SAST and supply-chain scan findings (CodeQL, Semgrep, ShellCheck, OSV-Scanner, Checkov) surface through code scanning as a required status check that is the merge gate. A maintainer does not quietly merge something one of these tools already knows how to flag.

The honest limit is narrower, and it is a limit every one of these tools shares by construction: each one matches against a catalog of what is already known to be bad, known query rules, known CVEs, known misconfiguration signatures. An OpenVEX disposition states a known vulnerability’s exploitability in this specific artifact, which presumes the vulnerability was already known well enough to have a VEX statement about it in the first place. A logic flaw or a genuinely novel vulnerability class that nobody has written a rule for yet is invisible to all of them, not from neglect, from scope: none of them were built to find that particular thing. That gap passes clean, gets attested, and the attestation is accurate about exactly that: nothing in the required predicate set found anything wrong. There is no deployed service here for a dynamic scan to probe either, so this pipeline does not carry the runtime-behavior check a DAST pass would add for a hosted application.

The manifest-review gate is a separate, narrower case, and it is explicitly soft-fail, so a structural violation shows up as a finding in code scanning, not as a blocked merge, unless a repository’s own branch protection turns that finding into a hard stop. The catalog App itself holds contents: write, pull-requests: write, and actions: write across every repo it is installed on, org-wide; that credential is the load-bearing trust anchor for the entire re-pin flow, and if it or the private key behind it is ever compromised, the attacker inherits exactly the privileges the hub was granted to do its legitimate job. Verification is also explicitly scoped to one signer identity per call, by design, which means a real compromise of a signer workflow itself, however unlikely given the isolation properties above, would not be caught by a check built to verify one specific, trusted identity in the first place. And every one of these mechanisms only runs at the moment code moves through the pipeline. Nothing in this system re-checks a plugin you already installed six months ago against anything new that surfaces about it today.

None of that is a reason to skip the pipeline. It is the reason “attested” cannot be the last word in your risk model. Attestation and SHA-pinning are a floor that rules out an entire category of attack, the cheap and common one, mutable-ref rug pulls and unverified provenance. They are not a ceiling on what a determined, patient, or already-trusted attacker can still do.

Two panels: what the attested pipeline covers, mutable-ref rug pulls, forged provenance, unverified releases, and the wrong signer identity, against what stays a human decision, a compromised credential, a maintainer merging a real bug that still attests cleanly, unenforced soft-fail findings, and when to move your own pin

The part that stays yours

So here is the actual rule, and it is not complicated. Pin every plugin your own project depends on to an exact SHA, not a floating tag or branch, in your own configuration, the same way this org pins its own catalog entries. Then decide, yourself, when that pin is worth moving. Before you move it, run the same verification this pipeline runs on itself: gh attestation verify against the release you are about to trust, or cosign verify-blob against the catalog you are about to pull from. If it does not verify, you already know not to touch it. If it does verify, you still get to decide whether this is the week you want that change, and what you are trading it for.

Do not let your assistant, or any automation acting on your behalf, make that upgrade decision for you. Not because the automation is untrustworthy in general; the mechanisms in this post exist precisely to make automated re-pins safer than manual ones. The reason is narrower and more personal: an upgrade to something that runs with your credentials is a decision about your own exposure, on your own timeline, and a SHA pin that only you move is a SHA pin whose history you can actually account for. An automatic bump you never reviewed, verified or not, is a decision you made without knowing you made it. Keep that decision. Verify what you are about to trust, then pin it, and move it again only when you say so.

Sources