All posts

How Dependency Guardian Catches the Shai-Hulud npm Worm

One preinstall hook. That's all it took. Within 72 hours, 796 npm packages were compromised, 20 million weekly downloads tainted. CISA issued an advisory. Microsoft and Palo Alto Unit 42 published writeups. The npm security team spent weeks revoking tokens and yanking packages.

Every postmortem focuses on how Shai-Hulud spread. The exponential growth, the credential theft chain. Nobody asks the more useful question: what does it take to stop it before it starts?

What Happened

A preinstall hook in package.json ran a 10MB obfuscated JavaScript file before npm install finished doing anything else. That script read ~/.npmrc to grab the developer's npm auth token, swept for GitHub PATs and cloud credentials, and shipped everything to a dead drop.

Then the worm went autonomous. Using the stolen npm token, it enumerated every package the developer maintained, injected the same payload into each one, bumped the patch version, and published. Every downstream developer who ran npm update got infected. Their tokens got stolen. Their packages got infected. A single hook turned into 796 compromised packages in three days.

Version 2, two months later, was worse. Heavily obfuscated payloads running through the Bun runtime to evade Node.js monitoring. Exfiltration through the victim's own GitHub account. A dead man's switch that wiped the home directory if the worm couldn't obtain valid tokens.

Why Your Scanner Said "0 Vulnerabilities"

Every CVE scanner — npm audit, Snyk, Dependabot — returned clean results on Shai-Hulud-infected packages. Zero vulnerabilities found.

That's not a bug. It's how they're built.

CVE scanners match package versions against a database of reported problems. Shai-Hulud wasn't exploiting a known vulnerability. The infected packages were real, popular libraries republished with an injected hook using the legitimate maintainer's stolen credentials. Valid token. Semver-compliant patch bump. Provenance checks passed because the attacker was the authorized publisher. Just not the person sitting at the keyboard.

No advisory existed because nothing was "broken" in the way these databases track. The worm used every security control exactly as designed. That's the gap CVE databases can't close, not after the next update, not ever. It's structural.

Patient Zero

The entire propagation chain traces back to one event. A preinstall hook reads ~/.npmrc on one developer's machine and exfiltrates the token.

That's patient zero. Everything else follows from it.

Block that hook, and the worm never starts. The credential is never stolen. The packages are never republished. Not "the damage is reduced." Zero packages compromised instead of 796.

Traditional tools detect this kind of attack weeks later, after someone notices anomalous publications, reverse-engineers the payload, files reports, and npm yanks packages one by one. By then, credentials from hundreds of developers are already sitting in an attacker's repo.

Dependency Guardian catches it before the merge. A preinstall hook that reads credential files and phones home matches patterns the scanner checks on every package entering your dependency tree. The PR check fails. The merge is blocked. The worm dies before it's born.

What Behavioral Scanning Does

Dependency Guardian doesn't check databases. It reads the code going into your dependency tree and flags what it does.

Install hooks executing obfuscated payloads. Code reading credential files it has no legitimate reason to touch. Outbound network calls with encoded data. Self-replication patterns: packages that try to publish other packages. When these signals show up together, the verdict is automatic. Block.

The approach works because it doesn't depend on someone else discovering the threat first. The behavior is the signal, whether the package was published ten seconds ago or ten months ago.

Try It

The GitHub App scans every PR that changes your lockfile. One installation, no config files, no pipeline changes. Or drop the CLI into any CI system:

npm i -g @westbayberry/dg

When scanning your dependencies, your source code stays on your infrastructure — only package names and versions leave your machine. (The optional pre-publish dg audit is the one feature that uploads, and only your own package, only when you opt in.)

Free tier: 1,000 scans a month with the full detection engine. Nothing gated behind a paywall.

Shai-Hulud won't be the last self-replicating npm attack. The technique is public and the trust model that made it possible hasn't changed. The question is whether you catch the next one at patient zero or read about it in a postmortem.