Your node_modules has cuckoos in it
A blind hours-old bird does the most efficient thing in nature, and it turns out npm has been doing it to itself for a year.
There's a bird, the common cuckoo, that doesn't build a nest. She finds someone else's — a reed warbler, say, halfway through the actual job of raising a family — and lays one of her eggs in it. Her egg hatches first. The chick that comes out is blind and naked. Within three or four days of hatching, before its eyes have even opened, it works each other egg up onto a small hollow in its own back, walks backward up the inside of the nest, and tips them over the rim. One by one. If there's a host chick that's already hatched, that goes over too. The warbler parents watch this happen and don't notice. Then they spend the next month feeding the cuckoo like it's their own kid.
This was first written up by Edward Jenner in 1788. Same Jenner. The smallpox-vaccine one. He wrote one of the foundational immunology papers in human history and also explained how cuckoos murder their nestmates while still blind. Some careers have range.
I bring this up because npm has been a cuckoo's nest for about a year now, and yesterday it had its worst day yet. The shape is identical: a trusted package gets quietly replaced, your build server raises it, and by the time anyone notices, your credentials are gone.
What just hatched in your nodemodules
On Tuesday, May 19, 2026, in about an hour, attackers published 633 malicious versions of real npm packages. Not weird squatted lookalikes. Real packages, with real maintainers, that real people use. jest-canvas-mock. size-sensor. echarts-for-react. timeago.js. A bunch of the @antv/ data-viz family. Two of them had been sitting clean on the registry, untouched, for three years. Dormant. Then suddenly a new version that exfiltrated every credential it could find from whatever machine ran npm install.
That was yesterday. The week before, node-ipc, 10 million weekly downloads. A few days before that, @tanstack/router and 41 of its siblings — 84 malicious versions in six minutes. In March, Axios, 100 million weekly downloads, hijacked maintainer account, two poisoned versions deploying a cross-platform RAT on macOS, Windows, and Linux. In April, Bitwarden CLI was impersonated on the registry for about ninety minutes on a Wednesday night. The password manager. The thing you install specifically because you were worried about credentials.
None of these were typosquats. Every one is a package you might have in your package.json right now, that you trusted last Tuesday, that someone built a legitimate business on. The pattern repeats every time: a maintainer's account or build pipeline gets borrowed, and a brand-new version of a trusted package ships with a payload bolted on. Your machine — or much more often, your build server — runs npm install. The cuckoo hatches.
If you want the current list of compromised package versions, Socket is maintaining it here. If you want to know how the worm actually works, TanStack's postmortem of the May 11 attack is the best writeup I've read on any of these.
Your CI is the bird
When a malicious package fires, it doesn't really care about your laptop. The juicy machine is the CI runner: the thing with your GitHub Actions secrets, your AWS deploy keys, your npm publish token, your Vault credentials, your Kubernetes service account material, and any OIDC tokens it can mint at runtime. Most of these payloads dump all of that to an attacker-controlled server within seconds of postinstall firing.
Then — and this is the part I admire in a craftsmanship way — one of the things they steal is your npm publish token. Which they use to publish your packages, with the same payload, into whichever poor downstream project is next to npm install. The worm grows. It's been getting louder every wave since September 2025.
The instinct, reading this, is to freeze everything. Pin every version forever. Never run npm update again. I sympathize, and that instinct is also wrong. Pin every dependency forever and your codebase accumulates known, public, exploitable CVEs like a basement accumulates spiders. Old versions are how shops got owned in 2019. That hasn't stopped being true. The next vulnerability scan will find them, and so will an attacker.
Auto-update on the other hand — npm update cheerfully, Dependabot configured to auto-merge minors, or worse, npm install some-package with no version specifier — and your build server will eventually catch a cuckoo. The day a real attack lands, the latest tag is poisoned for hours before anyone yanks it. You don't want to be standing under that tag with a bucket.
So the actual move is neither. Stay current, but not on the bleeding edge. Update on purpose, not on a Tuesday because Dependabot opened a PR while you were asleep. The gap between "fresh enough to be safe from old CVEs" and "fresh enough to be the next victim of a worm" is wide, and the registry gives you a way to live inside it.
Where SMBs get this wrong
A few failure modes I see in software shops:
"If it's on npm, it's fine." It's not. npm has roughly the trust model of a public bulletin board. npm install <name with no version. Your lockfile saves you for existing dependencies; this command picks up whatever was published forty seconds ago. Treating the lockfile as a build artifact you regenerate to silence merge conflicts, rather than a source of truth you read before you commit. Trusting the verified badge. The TanStack attack last week produced cryptographically valid SLSA Build Level 3 provenance. The badge was real. The code was poison. The badge proves the build pipeline ran. It doesn't prove the code that ran through it was clean. Either extreme: "we never update" or "we always grab latest." Both lose. One slowly, to CVEs; the other suddenly, to a worm.
The one thing worth understanding
A package isn't a thing. It's code that runs with your credentials, every time you install it.
That's the lens. Most developers think of an npm dependency as cargo — you put it on the boat, the boat goes, the cargo sits in the hold. It doesn't. The cargo has a postinstall script, and the boat is your build runner with the keys to every cloud account you own.
Once you see installation as code execution, the defenses fall out by themselves. You want to shrink what runs — turn off lifecycle scripts unless you actually need them. You want to shrink what's brand new — don't install a package version that's an hour old, because the malicious ones get caught and yanked within hours. You want to shrink what the running code has access to — scope your CI credentials, rotate them on a schedule, and keep your publish token separate from your deploy token.
In the npm world specifically, the first two of those collapse into two lines in your .npmrc that most teams don't have:
The first disables the lifecycle hooks that almost every recent attack used to land its payload. You'll occasionally have to run a setup script by hand for a package that genuinely needs one — that's the trade. The second tells npm to refuse any package version less than seven days old. Most malicious versions get caught and yanked within hours of publishing. Waiting a week dodges almost the entire cuckoo problem while a tired founder sleeps.
Pin your direct dependencies, commit your lockfile, and bump on a schedule that's yours, not someone else's. Run an automated dependency scanner so something is reading the CVE list for you, because you're not going to. That's the maintenance budget. It's smaller than you think.
One last thing
The warbler never figures it out. She raises the cuckoo, the cuckoo flies away, and the next spring she comes back and does the whole thing again. The species has run this arrangement since Aristotle wrote it down, twenty-three centuries ago.
We have a slight advantage over the warbler, which is that we can see our nests from the outside. We can read the postmortem, add two lines to a config file, and shut the cuckoos out before next Wednesday.