On a September morning in 2025, a maintainer of debug and chalk — packages with billions of weekly downloads between them — opened a convincing npm 2FA-reset email and did what it asked. The malicious versions lived for about two hours. A week later, a second campaign didn’t bother with phishing at all: Shai-Hulud, the first self-propagating worm in npm history, used tokens stolen three weeks earlier in the s1ngularity compromise of the Nx build system to trojanize over 500 packages, and CISA issued an emergency alert telling everyone to treat the related credentials as burned.
Parts 1 through 8 defended the running system — its data, identities, keys, and perimeter. This part defends the system that produces it, because 2025 settled the argument about where the perimeter is now: it’s your build pipeline. Not one of the year’s headline incidents used a zero-day. Every one ran on trust, long-lived tokens, and CI defaults that everyone had copy-pasted from everyone else.
The year the registry fought back
The wave is worth seeing as a sequence, because each incident ran on the previous one’s leftovers:
- tj-actions (March 2025) — a compromised GitHub Action dumped CI secrets into workflow logs across thousands of repositories, after a related
reviewdogaction was compromised first. Lesson one: a third-party action is a dependency with your secrets in scope. - s1ngularity (August 2025) — Nx was trojanized through a
pull_request_targetworkflow that executed attacker code with elevated permissions (a “pwn request”). The postinstall malware harvested tokens, SSH keys, and wallets — and, in a first, used the victim’s own AI CLI tools (Claude, Gemini) to assist its reconnaissance. - Shai-Hulud (September 2025) — the worm: a postinstall payload that steals npm/GitHub/cloud credentials, exfiltrates them to public repos literally named
Shai-Hulud, and republishes every package the victim can publish. Unrotated s1ngularity tokens were the seed — the most expensive skipped credential rotation of the year. - Shai-Hulud 2.0 (November 2025) — 700+ more packages, same playbook.
- TanStack (May 2026) — the one that should worry you most. No credential was stolen. The attacker poisoned
actions/cachefrom a fork PR, waited for the release workflow to restore it, extracted the OIDC token from runner memory, and published 84 malicious versions — with fully valid SLSA Level 3 provenance, signed by the real Sigstore pipeline. The bug was the CI design itself: a trust boundary crossed by a shared cache.
Provenance answers “where”, not “what”
The ecosystem’s response has been real and worth adopting: npm deprecated classic tokens after Shai-Hulud (granular tokens now live seven days), trusted publishing — OIDC-based, no long-lived secret at all — shipped in mid-2025, FIDO2 is replacing TOTP for maintainers, and Sigstore-backed provenance gives you a cryptographic receipt: this package was built on a hosted runner, from this commit, by this workflow.
Adopt all of it — and understand exactly what it doesn’t do:
Provenance verifies the kitchen, not the food. A package built by a poisoned pipeline from the real repository, at the real commit, attests beautifully. The TanStack incident is the proof of concept: the first documented malware with a completely legitimate Level 3 attestation. SBOMs sit in the same category — an inventory, invaluable at 3 A.M. when you must answer “do we depend on the thing that’s on fire,” but not a control. The stack is necessary and insufficient; what closes the gap is on your side of the install command.
The consumer’s checklist costs an afternoon
The good news: the highest-value defenses are boring configuration, and most of them are one line each:
- Freeze the lockfile in CI —
pnpm install --frozen-lockfileornpm ci. Semver drift is how a Tuesday becomes interesting. - Set a minimum release age — pnpm’s
minimumReleaseAge(npm addedmin-release-agein v11.10, and Dependabot/Renovate both have cooldowns). The debug/chalk window was two hours; most malicious versions are pulled within 24–72. A 24-hour age floor quietly dodges nearly the entire class. Time is the cheapest control in this article. - Allowlist install scripts —
onlyBuiltDependenciesin pnpm,--ignore-scriptselsewhere. Postinstall execution was the payload delivery mechanism in every 2025 incident. Almost nothing legitimate needs it; almost everything malicious does. - Verify provenance on what matters —
npm audit signatures, and prefer packages with the green checkmark. If you publish anything yourself, trusted publishing is the whole task: there is no token left to steal. - Turn on trust-downgrade detection — pnpm’s
trustPolicy: no-downgradeblocks a package whose publication suddenly drops from OIDC-attested CI to bare credentials. That exact downgrade was the s1ngularity signature.
Your CI is the most privileged system you own
Now the uncomfortable half. Your pipeline holds production deploy credentials, mints OIDC identities your cloud trusts, and — several times a day — executes code written by strangers who forked your repo. If you drew your architecture honestly, CI would be colored as “production-adjacent, internet-facing, runs untrusted code.” Nobody colors it that way. The rules follow directly from how the 2025–26 attacks actually worked:
pull_request_targetis a loaded gun. It runs with your repo’s secrets and permissions; if anything in the job checks out or executes the PR’s code, you’ve handed a stranger your keyring. Run untrusted code underpull_request(the fork’s permissions), or on an isolated runner with no credentials at all.id-token: writeonly on jobs that publish. The OIDC token is the crown jewel — tj-actions exfiltrated it from over-broad grants, and TanStack pulled it out of/proc/<pid>/mem. A job that doesn’t publish to npm or your cloud should never hold one.- Scope the cache to the trust level. A cache entry written by a PR-triggered job must never be restorable by the release workflow. This is the exact seam TanStack walked through, and it lives in your architecture, not in any scanner’s rule set.
- No secrets in PR-triggered jobs, and pin every action to a commit SHA. Tags move; SHAs don’t. The Shai-Hulud family monetized precisely these two defaults.
- Use OIDC to your cloud instead of stored keys. A pipeline that can’t leak a long-lived AWS key has one less incident in its future.
Notice what these have in common with the rest of the series: least privilege (Part 2), audit trails (Part 3), untrusted input treated as hostile (Part 4), boundaries drawn before they’re tested (Part 1). The supply chain isn’t a new discipline — it’s the same one, applied to the system you’d been trusting by default.
Where the series goes next
Current thinking for the next door: data privacy engineering — retention schedules that actually delete, residency, and the DSAR treadmill, where Part 8’s crypto shredding becomes the favorite tool. The security of the AI-agent development workflow itself — coding agents with repo access are the newest member of your supply chain — is the other strong candidate. As before: the order is decided when we get there.
Practice
- Spend one afternoon on the consumer checklist: frozen lockfile in CI, a 24-hour minimum release age, install-script allowlist,
trustPolicy: no-downgrade. Four diffs, and you’ve dodged the mechanism of every 2025 incident. - Audit your workflows with fresh eyes: grep for
pull_request_target,id-token: write, unpinned action tags, secrets in PR-triggered jobs, and any cache shared between PR and release workflows. Each hit is a TanStack-shaped seam. - Run the tabletop: “yesterday, one of our dependencies shipped malware for six hours.” Can you enumerate every deploy that contains it (this is what the SBOM is for), and can you rotate everything it touched within the hour? If either answer is no, you’ve found this quarter’s project.