On September 8, 2025, a maintainer known as "Qix" was phished with a fake 2FA-reset email spoofing the domain npmjs.help, handing an attacker a live session token and TOTP code. Within hours, malicious versions of debug (47 million weekly downloads) and chalk (299 million weekly downloads) — plus roughly 16 other packages including color-convert, strip-ansi, and wrap-ansi — were live on the registry, hooking window.ethereum and silently swapping cryptocurrency addresses in transit. The community caught it within a couple of hours and maintainers unpublished the malicious versions, but the incident was a preview: about a week later, the Shai-Hulud worm began self-propagating through npm, stealing maintainer credentials and using them to auto-publish itself into every other package those maintainers owned. It compromised 500+ packages before containment, and CISA issued an alert (AA25-266A) on September 23. A second wave, Shai-Hulud 2.0, backdoored 796 unique packages in November 2025 — proof the technique is now a reusable playbook for attackers, not a one-off. This post is the mirror-image playbook: what to do when it's your dependency tree.
How do you detect a worm before it spreads through your build?
Detection has to focus on behavioral indicators, because the malicious code itself is often a legitimate-looking patch-version bump. The debug/chalk and Shai-Hulud incidents both left the same class of artifacts: unexpected postinstall scripts that reach out to hosts your package never contacted before, new references to window.ethereum, fetch, or XMLHttpRequest in a utility library that has no business touching either, and outbound traffic to Discord webhooks, Telegram bots, or paste sites — all documented C2 channels in the Shai-Hulud analyses from Sysdig and Unit42. A second, worm-specific signal: private GitHub repositories flipping to public without a corresponding pull request, since Shai-Hulud exfiltrated stolen data by making victims' own repos public. Safeguard's Eagle classifier scores exactly these indicator classes — install-script behavior, egress patterns, and credential-harvesting reads from paths like ~/.npmrc and ~/.aws — across every npm publish, so a version bump that adds a wallet-address hook gets flagged before your CI ever runs npm install against it.
What do you do in the first 30 minutes after confirming compromise?
Treat it as an active worm, not a static incident, because the propagation mechanism means every minute of delay expands the blast radius through automated re-publishing. First, freeze all outbound npm publishing from CI and developer machines — pull NPM_TOKEN from CI secrets stores immediately, since Shai-Hulud specifically hunts for exactly that variable. Second, snapshot which packages and versions actually entered your build in the compromise window using your lockfile history or SBOM diff, rather than guessing from package.json ranges — ranges tell you what you allowed, not what you got. Third, isolate any CI runner or developer machine that ran npm install during the exposure window from further network egress until it's been checked for credential-harvesting activity, since the worm doesn't just infect a package, it infects the environment that installed it. Fourth, notify anyone downstream who consumes your own published packages, because if your CI credentials were live during the window, the compromise may already be one hop further than your own repo.
In what order should you rotate credentials?
Rotate in the order that matches how the worm actually uses stolen credentials to keep propagating, not alphabetically. npm publish tokens come first — revoke and reissue for every maintainer and every CI service account with publish rights, because a live token is what turns "we got infected" into "we infected our downstream consumers." Second, GitHub/GitLab personal access tokens and CI/CD pipeline secrets, since Shai-Hulud used compromised GitHub access both to make repos public and to plant its worm payload into CI workflows. Third, cloud credentials referenced in ~/.aws/credentials, ~/.gcloud, or environment variables on any machine that ran a potentially-infected install — these are explicit credential-harvesting targets, not incidental exposure. Fourth, any secrets that were readable from the same CI environment as the npm token, even unrelated ones, on the assumption that an attacker who got one secret enumerated the rest. Rotation should be org-wide, not scoped to the one maintainer whose account was phished, because both debug/chalk and Shai-Hulud spread laterally to packages the initial victim didn't directly maintain but had transitive publish access to.
How does dependency pinning actually stop this from recurring?
Pinning stops recurrence because both incidents relied on npm install or npm update silently pulling a newly-published, unreviewed patch version — the exact gap that semver ranges like ^4.0.0 are designed to permit. A committed lockfile (package-lock.json or equivalent) with npm ci enforced in CI means a malicious debug@4.4.2 published five minutes ago cannot enter your build until someone deliberately bumps the lockfile entry and that change goes through review. Disabling postinstall/preinstall scripts by default (npm config set ignore-scripts true, with narrow per-package exceptions) closes the exact mechanism both the debug/chalk payload and Shai-Hulud's self-propagation used to execute on install rather than on import. Pin to specific, audited versions for high-blast-radius transitive dependencies in particular — packages like chalk and debug sit in thousands of dependency trees precisely because they're low-level utilities everyone imports, which is why attackers target them.
How Safeguard helps
Eagle's indicator classes map directly onto both incidents' observed IOCs — install-script anomalies, egress to Discord/Telegram/paste infrastructure, and reads from ~/.npmrc and ~/.aws — and it classifies every npm publish, so a Shai-Hulud-style auto-publish gets scored before your policy layer ever admits it. Continuous scanning re-evaluates your existing SBOMs against Eagle's current classifier, so a package that looked clean at install time gets re-flagged within minutes of the model picking up a new malicious signature, not whenever you happen to re-run a scan. Combined with lockfile-enforced CI (npm ci, safeguard scan --fail-on critical,kev) and the Gold Registry's block on any artifact with a malware.* or typosquat.* finding at high confidence, the same controls in this playbook can run as policy rather than as a runbook you hope someone remembers during an active incident.