You checked that your dependencies were signed and provenance-verified, and the Shai-Hulud npm attack proved that check answers the wrong question. Provenance records where a file came from. It says nothing about whether the build that produced it was compromised before it started.
## What the Shai-Hulud npm attack actually did
The Shai-Hulud npm attack was a self-propagating supply-chain worm, published in September 2025, that compromised 444 package names across roughly 2,234 poisoned versions using credentials from 12 different organizations. The first package hit was keyv, a small storage abstraction library, followed within the hour by the caching packages built on top of it: flat-cache, file-entry-cache, and cacheable.
The sequence was fast. A commit landed on the keyv repository in the morning, a signed release followed 33 minutes later, and by mid-morning the worm was publishing itself into unrelated packages. The payload included a credential sweep targeting stored API keys for AI tools, including Claude Code, OpenAI, Codex, Cursor, and the Gemini CLI. Agent keys sit in dot files in a home directory, rotate rarely, and are treated as loot in this campaign.
One detail made triage harder: the malicious commits carried an author string referencing Claude, pointing anyone skimming history toward an AI agent rather than the intruder. Nine days after the incident, no attribution had been published and the original account break-in remained unexplained.
## Why trusted publishing and valid signatures did not stop it
Trusted publishing did not fail because the attacker bypassed it. It failed because the attacker used it. After the September 2025 attack on chalk and debug, where a phishing mail led to a crypto wallet drainer shipping in packages pulled billions of times a week, npm revoked classic no-expiry tokens in December, capped write tokens at 90 days, made two-factor the default, and pushed maintainers toward trusted publishing, which replaces long-lived tokens with short-lived pipeline identity and a signed provenance attestation.
In the Shai-Hulud npm attack, the intruder pushed a commit to the repository's main branch. The project's own GitHub Actions workflow checked out the tagged code, built it, published it, and signed an attestation stating exactly that. Every claim the attestation makes was true: the build was real, the source matched the tag, the signature was valid, and the artifact was malware.
Snyk's write-up drew the boundary plainly: provenance can faithfully attest a build whose source or workflow context has already been compromised. Provenance answers a question about origin. It was never designed to answer a question about intent. A year of registry hardening assumed the attacker stood outside the trusted path; this one was already inside it, holding the maintainer's account.
## The attack moved from npm install to editor and agent hooks
The install script was close to a decoy. npm version 12 had already made dependency install scripts opt-in, a change made after an August 2025 worm used a build configuration file for free code execution and spread through 57 packages in under two hours. The worm responded by moving up one layer: alongside the payload, the commit added configuration to two directories inside the repository itself, one belonging to Visual Studio Code and one belonging to Claude Code.
Snyk Code's analysis counted the entry points and found that two of the three triggers never touch node_modules. The editor task runs when the folder opens; the agent hook runs when a session starts. A developer can be compromised without ever running npm install, which means deleting node_modules does nothing and a fresh checkout reinstates the trigger from the repository.
The pattern had been field-tested. The earlier June worm planted configuration in five places at once: Claude Code, the Gemini CLI, Cursor, Visual Studio Code, and the test script in package.. SafeDep, which tracks these campaigns, summarized the shift in one line: same loader, same payload, same infrastructure, different detonation surface. Close the install path and the attack moves to the clone path, which makes every editor and agent config directory effectively executable code that belongs in code review.
## How bad was it really? The 2 billion figure, checked
The headline number needs handling before it gets quoted. Two billion is a monthly download figure for the compromised set. The chalk and debug attack eleven months earlier reached roughly two billion downloads per week, about four times the scale, and the two numbers get repeated as if they described the same event.
The downloads also overlap enormously. The dependency chain runs file-entry-cache → flat-cache → cacheable → keyv: four packages, all compromised, one chain. Installing the top of the chain ticks every counter beneath it, which is why their monthly numbers sit within a few percent of each other. Three of the four account for about 90 percent of the two billion, and Snyk's own write-up noted that these figures measure ecosystem reach, not compromised hosts. Upwind, which measures real deployments rather than downloads, counted keyv in about 85,000 environments.
Semantic versioning did quiet defensive work too. The poisoned keyv release was a major version bump, the one thing a caret range refuses to install automatically, so most of that volume could never reach it. The worm understood this: when it republished itself into other maintainers' packages, it incremented the patch version, the one place a caret range says yes without asking. The numbers that actually matter are 2,234 poisoned versions across 444 package names, published with credentials from 12 organizations.
## The one control that works: a publish cool-down
The control that would have stopped this for almost every team is the least impressive idea available: refuse to install any version published in the last 24 hours. The poisoned keyv release was live for 1 hour and 43 minutes before its registry record changed, and every version in that wave was unpublished afterward. A cool-down needs no scanner, no threat feed, and no vendor. It simply arrives late enough that someone else has found and extinguished the fire.
pnpm enables this by default through its minimumReleaseAge setting, documented in the pnpm settings reference. npm has shipped the equivalent setting since February 2026 and leaves it off. The cost is real: you will be a day behind on genuine security patches, and it does nothing for the maintainer whose account was stolen in the first place. Most teams should still take that trade.
What the idea to retire is the sense of safety a signature provides. A signature tells you where a file came from and has never told you what is inside it. The trust boundary has moved from the registry to anywhere code can run, which now includes a folder opening in an editor and a session starting in an agent.
## A short response checklist
For teams auditing their exposure after the Shai-Hulud npm attack, four steps cover the practical ground:
- Pin the
minimumReleaseAge(or pnpm equivalent) setting in every project so fresh publishes cannot install for 24 hours. - Review
.vscode/and agent configuration directories (Claude Code, Cursor, Gemini CLI) in pull requests as executable code, because that is what they are. - Rotate AI tool and registry credentials stored in home-directory dot files, and apply the same secret-handling discipline to them as to cloud keys.
- If you cleaned only one of the two compromised config directories, clean both; Snyk's responder guidance warned that cleaning one leaves the other path live and a fresh checkout restores both.
## FAQ
- Was the npm signature on the malicious package fake? No. The signature and provenance attestation were genuine because the attacker pushed the malicious commit into the repository itself, and the project's own trusted publishing pipeline built, published, and signed it. Nothing was forged.
- Did npm install scripts spread the Shai-Hulud worm? Not mainly. npm had already disabled install scripts by default, so the worm planted triggers in editor and agent configuration directories that fire when a folder opens or an agent session starts, without requiring npm install.
- Were 2 billion installs compromised? No. Two billion is the combined monthly download reach of the 444 compromised package names, not a host count. Overlapping dependency chains inflate the total, a major-version bump blocked most caret-range installs, and Upwind measured keyv in roughly 85,000 real environments.
- What single change protects most projects today? Enable a publish cool-down such as
minimumReleaseAgeso versions younger than 24 hours cannot install. The poisoned releases were unpublished within hours, so a one-day delay blunts this class of attack without detecting anything itself.
## From video breakdown to written record
This article grew out of a video breakdown of the incident, the kind of analysis that usually stays locked inside a timeline nobody can search. The same cycle of community coverage, from the Dev doido write-ups to the Crazystack typescript notes at crazystack.com.br, shows how much incident knowledge lives in scattered formats. If you have lessons like these sitting in your own YouTube videos, Skala Blog turns them into a written article: paste the URL, transcribe the video, and publish the result.
A signature proves origin, not intent. If that distinction took an hour of video to explain, it deserves a page a search engine can find.
Fork this article
Start a new branch from the same video, shaped your way. You keep the credit; the original keeps the attribution.
A fork in another language is filed as a translation of this article, so the two pages point at each other. You can unlink it later from the editor.
0/240
You are creating
- Format
- For
- Language
- Source
- Your angle
You will be asked to sign in before it is generated.
Buy credits