Shai-Hulud npm worm
Shai-Hulud was a self-replicating worm that spread through the npm registry in 2025, compromising hundreds of packages across two waves. Unlike a one-off package compromise, it propagated on its own: once it stole a maintainer's credentials, it republished itself across every package that maintainer owned.
What happened
The campaign unfolded in two main waves (with smaller copycat bursts in between).
Wave 1: September 2025
First disclosed around September 15, 2025, the initial wave began with the popular
@ctrl/tinycolor package and grew to hundreds of packages (roughly 500 by several trackers). Once a
package was installed, the payload, triggered by a postinstall lifecycle script, ran
the open-source secret scanner TruffleHog across the machine and CI environment to harvest npm tokens, GitHub
personal access tokens, and cloud credentials. It exfiltrated what it found by creating a public
GitHub repository on the victim's account and committing the stolen data, and it planted a malicious GitHub
Actions workflow for persistence. Crucially, it then used stolen npm tokens to enumerate and republish
trojanized versions of the victim's other packages: the self-propagating "worm" loop.
Wave 2: "The Second Coming," November 2025
A second, larger wave ran from roughly November 21 to 24, 2025, affecting on the order of 800
packages. It shifted execution from postinstall to preinstall, so the payload ran
even earlier in npm install. It bundled the Bun runtime and a TruffleHog binary, registered
infected machines as self-hosted GitHub Actions runners for persistence, and, if it could not
exfiltrate or spread, attempted a destructive fallback that wiped the user's home directory.
chalk, debug, and related packages, delivered
a crypto-clipper that swapped wallet addresses. It is frequently mentioned alongside Shai-Hulud because both
hit npm the same month, but it is a different attack with different behavior.
The settings that reduce exposure
-
Blocking install scripts (ignore-scripts). Shai-Hulud's
entire execution depended on npm lifecycle scripts firing automatically during install:
postinstallin Wave 1,preinstallin Wave 2. With install scripts blocked (or gated behind an approval allowlist), a malicious package lands as inert files innode_modulesand the payload never runs: no secret scanning, no exfiltration, no re-publishing. - Dependency cooldown (minimum release age). The trojanized versions were detected and pulled within hours. A cooldown that holds back brand-new versions for several days means your installs skip the poisoned releases entirely while the registry and community remove them.
- Failing on unreviewed build scripts (strict-dep-builds, pnpm). Turning the silent "this dependency wants to run a build script" case into a hard error forces a review instead of letting it slip through.
ignore-scripts breaks packages that genuinely need build steps,
so an allowlist model is the practical form; and a cooldown does not help against a compromise that stays
undetected for longer than your quarantine window. Layer these controls rather than relying on any one.
Harden your setup
DepsGuard checks whether install-script blocking, a cooldown, and the other built-in defenses are enabled for the package managers you have installed, and turns on the missing ones, with a diff preview and a backup before any change.
Sources
- CISA: Widespread Supply Chain Compromise Impacting the npm Ecosystem (Sept 2025)
- Socket: @ctrl/tinycolor supply chain attack (Sept 2025)
- Palo Alto Unit 42: npm supply chain attack analysis
- Datadog Security Labs: Shai-Hulud 2.0 deep dive (Nov 2025)
- The Hacker News: Second wave affects 25,000+ repositories (Nov 2025)