A self-propagating worm called Shai-Hulud has compromised over 1,300 package versions on the npm registry, representing a combined 2 billion monthly downloads. The most recent wave, which Microsoft tracks as Mini Shai-Hulud, hit the keyv package and eight related packages on August 4, 2026 after an attacker compromised a maintainer's GitHub account. Keyv alone had over 600 million downloads in the preceding month.
If you ran npm install on a project in early August without a locked dependency tree, this is worth twenty minutes of your attention.
What actually happened
The attack is a worm, which is the part that makes it different from the usual typosquatting story. It does not just sit in one malicious package waiting to be installed. Once it lands in an environment, it hunts for credentials, uses those credentials to publish infected versions of other packages that maintainer controls, and repeats.
That self-propagation is why the blast radius kept growing in waves rather than being contained after the first disclosure. CISA issued an alert on the initial compromise in September 2025. Elastic Security Labs documented a later variant, CHAINDROP, hitting more than 400 npm packages. Microsoft published guidance on Shai-Hulud 2.0 in December 2025, then a further analysis of the ChainDrop self-propagating worm in August 2026. Palo Alto's Unit 42 has been maintaining a running tracker across the whole campaign.
The compromised packages were not obscure. Waves of this campaign have affected packages belonging to TanStack, Mistral AI, UiPath, and OpenSearch. In the August keyv incident, the surrounding blast radius included flat-cache at roughly 580 million monthly downloads, cacheable-request at over 137 million, cacheable at over 30 million, and cache-manager at over 16 million.
Most developers reading this have never deliberately installed any of those. They arrive as transitive dependencies, four levels down from something you did choose.
How the payload runs
The malicious versions carry heavily obfuscated payloads designed to execute during installation or on import. That first path is the one people underestimate. An npm lifecycle script runs code on your machine before you have imported a single line of the package:
You do not have to use the package. You do not have to import it. Installing it is enough. That is the entire attack surface, and it has been sitting in npm's design since the beginning.
What to actually do about it
Disabling lifecycle scripts by default is the single highest-value change, and it takes one line:
This breaks packages with legitimate native build steps, which is why nobody turns it on by default. In practice the list of packages in a typical project that actually need a postinstall script is short, and you find out immediately which ones they are.
Beyond that, the measures that would have limited this specific campaign:
- Commit your lockfile and use npm ci in CI rather than npm install. A lockfile pins exact versions, so a newly published malicious 9.9.9 does not get pulled into a build that was previously reproducible.
- Set a cooldown before adopting new versions. Most of these compromises were detected and pulled within hours to days. Installing nothing published in the last week costs you almost nothing and skips the window when these payloads are live.
- Rotate any credential that was present in an environment where a compromised version was installed. The worm's whole purpose is credential theft, and an npm token it captured is what lets it spread to your packages next.
- Enable two-factor authentication and trusted publishing on any package you personally maintain. The August keyv incident started with a compromised maintainer GitHub account, not a flaw in npm.
Checking whether you were exposed
Start with what is actually installed rather than what your package.json claims:
The advisory databases from CISA, Microsoft, and Unit 42 list specific affected version numbers, and those lists have been updated repeatedly as new waves landed. Check the current advisory rather than a blog post's snapshot, including this one.
Why this keeps happening
OWASP moved software supply chain risk into its own category for 2025, listed as A03:2025 Software Supply Chain Failures, absorbing and broadening what used to be filed under vulnerable and outdated components. The 2025 revision explicitly shifted focus toward the security of the whole software ecosystem rather than isolated coding flaws in your own repository, and campaigns like this are the reason.
The uncomfortable truth is that the average JavaScript project trusts several hundred people it has never heard of, each of whom can push code that runs on your machine and in your CI. No amount of careful coding in your own repository addresses that. The mitigations above are all about reducing what a compromised dependency can reach, because preventing compromise upstream is not something you control.
My take
I turned off install scripts on every project I maintain after the August wave and have hit exactly two packages that needed an exception. That ratio suggests the default has been wrong for a decade. The ecosystem is slowly moving toward provenance attestation and trusted publishing, both of which genuinely help, but adoption is partial and a worm only needs the gaps.
Treat every dependency as code you are choosing to run with your own permissions, because that is exactly what it is.
Sources: CISA alert, Palo Alto Unit 42 tracker, Microsoft Security analysis of ChainDrop.