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.

Not the same as the chalk/debug incident. A separate September 2025 npm attack, the "Qix" phishing compromise of 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: postinstall in Wave 1, preinstall in Wave 2. With install scripts blocked (or gated behind an approval allowlist), a malicious package lands as inert files in node_modules and 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.
Caveats worth knowing: a blanket 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