September 14, 2025. Researchers named it Shai-Hulud, after the sandworm in Dune. By the time npm’s incident team finished revoking tokens and yanking versions, 500+ package releases had been compromised — some of them carrying millions of weekly downloads.

The mechanism was embarrassingly simple. Not a zero-day. Not a novel attack class. A design affordance that has been sitting in the npm runtime since post-install hooks were added, waiting for someone patient enough to use it.

How It Worked

Entry was credential stuffing and phishing against maintainer accounts — specifically accounts holding unrotated tokens from a previous breach. The attackers didn’t need to be clever. npm’s token hygiene in the average CI pipeline is a disaster. Tokens generated once, scoped to everything, rotated never. They knew going in that getting one credential meant access to multiple packages. That’s not an assumption; that’s the industry default.

Once inside a maintainer account, they injected malicious postinstall scripts into popular packages and published. When a developer — or a CI runner, or a Docker build — installed one of those packages, the postinstall script ran. It scanned the environment for npm tokens. If it found one, it called npm publish on every package that token had rights to, injecting the same script into each new release.

That’s the worm. Each infected installation becomes a vector. Each compromised token fans out to however many packages it can write. If you had one token scoped to ten packages, Shai-Hulud ate all ten, and every subsequent install of any of those ten packages could harvest a new token and repeat the cycle.

Trust inheritance is what made it geometric. npm’s access model doesn’t care whether a publish comes from a human or a postinstall hook — if the token is valid, the publish goes through.

Why Detection Took So Long

Post-install scripts calling npm publish looks like CI. That’s the thing. It looks exactly like a normal automated release pipeline. Nobody is alerting on “package published from inside a build.” That’s literally how some projects do releases. The signal was invisible in the noise until the volume became impossible to ignore.

By the time the pattern surfaced, it had already propagated multiple generations deep.

The Name Is More Apt Than Whoever Named It Probably Realized

In Dune, Shai-Hulud isn’t something you defeat in a direct fight. You don’t kill the worm by standing in front of it. You survive by not making the vibrations that attract it — by moving differently, by not announcing yourself to the substrate.

The npm parallel is precise. You don’t fix this class of attack by building better malware detection in the registry. You fix it by not creating the substrate the worm needs to travel.

Least-privilege tokens. Scoped per-package. Rotated on a cadence. Read/write separation between CI and publish credentials. None of this is exotic. All of it is operationally painful, so almost nobody does it.

The “embarrassingly simple” framing isn’t about the attackers. It’s about us. The worm substrate — “scripts execute at install time” plus “tokens live in environments” plus “tokens can publish” — has been there for years. Anyone paying attention could have built this. The surprise isn’t that it happened. The surprise is that it took this long.

The Structural Problem

npm’s trust model is built around maintainer identity. A valid token proves who you are. It proves nothing about what you’re publishing or where that publish originated. If your token is in an environment and something in that environment calls npm publish, npm’s response is: authenticated, proceeding.

Sigstore and TUF exist to address a different axis of this — signed provenance, verified build pipelines, artifact attestation that ties a package release to a specific source commit and build environment. If that infrastructure had meaningful adoption, a postinstall-spawned publish from an arbitrary developer’s machine would fail provenance checks and get flagged. The token alone wouldn’t be sufficient.

Adoption is thin. The ecosystem agreed that signed provenance was the right answer sometime around 2022 and then mostly continued not doing it.

What Changes

npm’s immediate response was mass token revocation and yanking affected versions. That’s the right emergency move. It stops the bleeding.

Longer term, nothing structurally changes unless publish access gets scoped in a way that can’t be inherited by a subshell spawned from postinstall. The npm security team has been pushing granular access controls and audit logging improvements. Workbrew’s reporting on the incident flagged mandatory 2FA expansion as a near-term mitigation.

All of that helps at the margins. The real fix is treating npm tokens the way you’d treat AWS credentials in a production environment — least privilege, short-lived, audited, never ambient in a general-purpose build environment. Not because it’s clever security policy, but because the alternative is that your npm install is a lateral movement vector.

Shai-Hulud was elegant in a horrible way. It used the ecosystem’s own mechanics against itself. The packages spread it. The tokens fed it. The trust model carried it forward.

The worm is gone. The substrate is still there.


PGP signature: shai-hulud-the-npm-worm.md.asc — Key fingerprint: 5FD2 1B4F E7E4 A3CA 7971 CB09 DE66 3978 8E09 1026