Shai-Hulud: The Malware With a Valid Signature
On 4 August somebody got into the GitHub account of the maintainer behind keyv, a small caching library that almost nobody thinks about and roughly 127 million weekly npm downloads depend on.
What happened next is worth describing at its actual speed. The attacker published a poisoned release. Every machine that installed it ran a script before anything else happened, which harvested that machine’s credentials, and among those credentials were npm publishing tokens. The malware used the stolen tokens to republish the victim’s entire namespace, at roughly one package per second, each new package carrying the same payload. Then it did it again from the next set of stolen credentials. Researchers watched it move from one organisation to the next every two to seven minutes. Within half an hour it had reached nine unrelated organisations, including Deliveroo, Qlik and ServiceTitan, none of which had any relationship with each other beyond a shared package registry.[1]
Counts vary by who was measuring and when, which is what happens when the thing you are counting is still growing. Aikido put it at 868 packages across 1,381 versions. SafeDep verified 353 poisoned versions across 79 package names, with a wider suspected footprint. Other trackers watched 50 to 100 new packages appear every few minutes and passed 1,280.[1][2] Call it a thousand packages and two billion monthly installs and accept that the number was wrong by the time it was published.
Here is the part I cannot stop thinking about. Those malicious releases were, in the cryptographic sense, entirely legitimate. They were built and published through the maintainers’ own continuous integration pipelines. They carried valid provenance. If you had checked the signature, the signature would have checked out.
What actually runs when you type install
Start with the mechanism, because the mechanism is the argument.
When you run npm install, packages are permitted to execute scripts on your machine as part of being installed. This is not a vulnerability. It is a documented feature, and plenty of legitimate packages rely on it to compile native code or set themselves up. The lifecycle hook this worm used is preinstall, which fires before the package contents are even placed, and long before any human could plausibly inspect what arrived.
The payload itself is not subtle. The preinstall hook pulls down the Bun runtime and uses it to execute a heavily obfuscated stealer of around 728 kilobytes.[2] That stealer sweeps the machine for anything that grants access to something: npm authentication tokens sitting in .npmrc, GitHub CLI tokens and session tokens, AWS credentials, HashiCorp Vault tokens, Kubernetes configs, and cryptocurrency wallets.
Now notice the loop that makes this a worm rather than a theft. The credentials it steals are the credentials it needs to spread. An npm publishing token is both loot and fuel. Every developer machine and every continuous integration runner that installs a poisoned package becomes a launch point for the next wave, and continuous integration runners are the ideal host, because they hold production credentials, they run unattended, and nobody is watching the install log at three in the morning.
That is how you get from one compromised account to nine organisations in thirty minutes. Not through a clever exploit chain, but because the ecosystem is built so that installing software means running the author’s code, and because the thing worth stealing is the thing that lets you keep going.
The signature was valid
This is where the story stops being an ordinary breach and starts being interesting.
Over the past few years the industry did the responsible thing about supply chain security. We built package signing. We built provenance attestation, so a package can prove it was produced by a specific build pipeline from specific source. npm supports trusted publishing, where a project publishes from its continuous integration workflow using a short-lived identity token rather than a long-lived secret that can leak. This is genuinely better engineering than what came before, and I want to be fair to the people who built it.
It did not stop this.
The clearest demonstration came in the May wave of this same malware family, which hit the TanStack projects. The attacker extracted GitHub Actions OIDC tokens directly out of runner memory, and used them to publish through TanStack’s own trusted publisher relationship. The result was 84 malicious versions across 42 packages, every one carrying a valid Level 3 SLSA provenance attestation.[3] The supply chain security apparatus examined the malware and certified it as authentic, because by its own definition it was.
The lesson generalises, and it is worth stating precisely. A provenance attestation proves which pipeline produced an artifact. It does not prove that the pipeline was still under the control of the people who built it. Those are different claims, and only one of them is the claim anybody actually cares about. We spent years learning to verify the second-best question because it was the one we knew how to answer with mathematics.
I have made a version of this argument before, about a different technology. A hardware wallet guarantees that a secret never leaves the device, and the Coldcard entropy failure I wrote about last week honoured that guarantee completely while the coins left anyway, because the attacker did not need the secret to leave when the secret was guessable. The guarantee was kept. It was the wrong guarantee. Signatures here are the same shape of mistake: an exactly-honoured promise about the wrong property.
None of which means signing is worthless. It means signing answers “where did this come from” and we have been reading the answer as “is this safe.”
Three years of the same lesson, learned by the attacker
What makes this family instructive is that you can watch it adapt, wave by wave, to whatever defence was deployed against the last one.
The first Shai-Hulud arrived in September 2025. Patient zero was a package called rxnt-authentication, and the malware ran from a postinstall script and exfiltrated what it found to public GitHub repositories it created. Over 200 packages and more than 500 versions were compromised in four days, and CISA issued an alert.[4]
November 2025 brought the second wave, and it had already moved. Execution shifted from postinstall to preinstall, which fires earlier and gives defenders less room. The payload arrived through a setup_bun.js script that dropped an obfuscated bun_environment.js, using a legitimate runtime to run malicious code, a technique that makes static scanning much harder. That wave backdoored 796 unique packages.[5]
May 2026 was the adaptation that should have made more noise than it did. Attackers stopped trying to defeat trusted publishing and started stealing the identity that trusted publishing runs on, which is how the TanStack packages ended up correctly attested. In the same wave the malware learned to persist somewhere new, which deserves its own section.
And now August, running the accumulated playbook: preinstall, Bun, obfuscation, credential theft, namespace republishing, and enough speed that the incident outpaced the reporting.
Each generation is a direct response to the previous defence. This is not a series of unrelated incidents. It is one adversary iterating against us in public, and iterating faster than the ecosystem changes its defaults.
It lives in your editor now
The May wave introduced something I think is genuinely new, and it is the detail most relevant to how many of us now work.
The malware writes persistence into the configuration files that AI coding assistants read automatically. It adds hooks to .claude/settings.json for Claude Code, and tasks to .vscode/tasks.json with runOn: folderOpen for VS Code, so the payload re-executes every time a project is opened. It also installs a system daemon, a LaunchAgent on macOS or a systemd unit on Linux, so it survives a reboot. All of this survives npm uninstall, because none of it lives in node_modules any more.[6]
Then it does the thing that turns a nuisance into an infestation. It scans the filesystem for every other Claude Code and VS Code configuration it can find and injects the same hooks into all of them. One poisoned dependency, in one project, and the persistence layer is now in every repository on that machine.[6]
Sit with the shape of that. Your coding assistant, the tool you invited in specifically because it can read and act on your projects, is now the re-infection vector. It has file access, it runs automatically, it is trusted by construction, and its configuration is a place almost nobody thinks to audit. I wrote in an essay a couple of weeks ago about autonomous systems reaching places they were never given, and argued that containment tends to be theatre because the failure is upstream in the human choices. This is the same story arriving from the other direction. Nothing here is an autonomous agent doing anything clever. It is ordinary malware that noticed we had installed something with broad permissions and a config file, and moved in.
The part that will annoy people like me
The command-and-control for the August wave runs through an Ethereum smart contract, at an address the researchers published, which the attacker uses to rotate infrastructure without ever modifying the payload.[1]
I have spent my career arguing for systems that no single party can switch off. That is not a slogan for me, it is what I build. So I am not going to pretend this is somebody else’s problem. A censorship-resistant, always-available, permissionless coordination layer is exactly as useful to a worm operator as it is to everybody else, and for exactly the same reasons. You cannot seize the domain, because there is no domain. You cannot serve a takedown on a contract.
The honest position is not to deny the tradeoff or to conclude that permissionless infrastructure was a mistake. It is to notice that the same property shows up on both sides of every ledger, and that anyone who tells you their technology only has good uses is selling something. I made the same admission about open model weights, and I would rather make it consistently than only when it is comfortable.
Why “rotate your tokens” is not enough
The standard advice after a credential-stealing incident is to rotate everything. Here that advice is incomplete in a way that matters, and the incomplete version can leave you feeling safe while you are not.
The malware installs a watcher for credential revocation. Responders are advised to remove that watcher first, because rotating tokens while it is still running is not a clean operation.[2] Meanwhile the persistence lives in your editor config and in a system daemon, so removing the package accomplishes nothing.
This is a specific instance of something I have argued more generally in an earlier piece: advice that assumes a clean, cooperative environment fails precisely when the environment is neither. Any machine or runner that installed an affected version should be treated as fully credential-exposed, not partially, and the order of operations matters.
Underneath it all is a design fact nobody wants to look at directly. When you install a package with a handful of dependencies, you are not trusting a handful of packages. You are trusting every maintainer account in the transitive tree, plus the security of the platforms those accounts live on, plus the continuous integration pipelines that publish them. That is a trust surface of thousands of humans and their session tokens, and preinstall gives every one of them arbitrary code execution on your machine by default. We call this “adding a dependency.”
What actually holds
The mitigations that work are unglamorous, which is a pattern I keep running into when I write about security.
Turn off lifecycle scripts. Installing with --ignore-scripts, and enabling it in your project or global npm configuration, removes the entire preinstall execution path that this family depends on. It breaks a few packages that genuinely need to build native code, and you allow those deliberately. Most teams have never even considered this, which is the point.
Pin what you install. Lockfiles, exact versions, and a delay before adopting new releases all cost you very little and remove the window in which a freshly poisoned version reaches you automatically. A worm that republishes a namespace at one package per second is only dangerous to people whose tooling will take the newest thing without asking.
Separate the credentials that publish from the machines that develop. The reason a single developer laptop can poison an entire namespace is that the same machine holds the publishing token, the cloud credentials, and an editor running arbitrary downloaded scripts. GitHub has moved in the right direction here, requiring two-factor authentication for local publishing and shortening token lifetimes to seven days.[3] That reduces the blast radius. It does not close the OIDC path that the May wave used, and it should not be mistaken for having solved the problem.
And keep the things that must not move off the machine entirely. This worm steals cryptocurrency wallets alongside cloud credentials, and the only category of secret that came through this untouched is the category that was never on the machine in the first place. A key that lives in hardware and signs without ever being exported cannot be swept up by a preinstall script, no matter how thoroughly that script owns your laptop. That is the whole argument for holding your own keys properly, and it is the one guarantee in this story that held exactly as advertised.
The uncomfortable summary is that our supply chain defences are excellent at proving that a package is what it says it is, and nearly useless at telling you whether the person who said it is still the person you think. Identity is the soft layer under all of the cryptography, and identity is a GitHub account with a session token on somebody’s laptop.
We built a machine that can prove where the code came from, and taught ourselves to hear it saying the code is safe.
1. Aikido Security. (2026). Keyv and friends compromised in active Shai-Hulud supply chain attack. https://www.aikido.dev/blog/keyv-and-friends-compromised-in-npm-supply-chain-attack · BleepingComputer. (2026). Massive ChainDrop npm supply-chain attack infects hundreds of packages. https://www.bleepingcomputer.com/news/security/massive-chaindrop-npm-supply-chain-attack-infects-hundreds-of-packages/
2. SafeDep. (2026). npm worm poisons 400+ packages across nine organisations. https://safedep.io/keyv-npm-supply-chain-compromise/ · The Hacker News. (2026). Keyv-linked npm worm poisons hundreds of packages, plants Claude Code and VS Code hooks. https://thehackernews.com/2026/08/keyv-linked-npm-worm-poisons-hundreds.html
3. Snyk. (2026). Mini Shai-Hulud hits AntV: 300+ malicious npm packages published via compromised maintainer account. https://snyk.io/blog/mini-shai-hulud-antv-npm-supply-chain-attack/ · ReversingLabs. Will new npm security measures stop the next Shai-Hulud? https://www.reversinglabs.com/blog/npm-security-shai-hulud
4. Cybersecurity and Infrastructure Security Agency. (2025). Widespread supply chain compromise impacting npm ecosystem. https://www.cisa.gov/news-events/alerts/2025/09/23/widespread-supply-chain-compromise-impacting-npm-ecosystem · Unit 42, Palo Alto Networks. (2025). “Shai-Hulud” worm compromises npm ecosystem in supply chain attack. https://unit42.paloaltonetworks.com/npm-supply-chain-attack/
5. Datadog Security Labs. (2025). The Shai-Hulud 2.0 npm worm: analysis, and what you need to know. https://securitylabs.datadoghq.com/articles/shai-hulud-2.0-npm-worm/
6. Sonar. (2026). Mini Shai-Hulud targets AI coding agents. https://www.sonarsource.com/blog/mini-shai-hulud-targets-ai-coding-agents/ · Akamai. (2026). Mini Shai-Hulud: the worm returns and goes public. https://www.akamai.com/blog/security-research/mini-shai-hulud-worm-returns-goes-public