A Brief History of Shai-Hulud, the npm Worm
By Patrik Tang on
Since September 2025 a self-replicating npm worm called Shai-Hulud has come
back six times. It works like this: land on a machine, harvest every
credential in reach, use the ones that can publish to infect more packages.
What changes each wave is where the code runs: postinstall, then
preinstall, then a poisoned CI cache, then a native build step, then a
Python import. The August
2026 wave, which took the keyv and cacheable family, was blocked outright
for anyone already on npm 12 or running with ignore-scripts.
On August 4, 2026, someone took over the GitHub account behind keyv and
cacheable and pushed malicious commits straight to main. Within minutes the
same payload was in ten more caching packages from the same maintainer, and
from there the worm kept going on stolen tokens: 400+ packages across 1,700+
versions. keyv alone has around 127 million downloads a week, and two of
its siblings, flat-cache and file-entry-cache, sit underneath ESLint.
-
Shai-Hulud
The original. A
postinstallscript runs TruffleHog, and the secrets land in a public repo on the victim's own account. Around 500 packages, including CrowdStrike's. -
The Second Coming
Entered through a malicious commit in
asyncapi/cli.preinstall, 796 packages in the end. - The Third Coming, then Mini Backdoored the Bitwarden CLI through a trusted GitHub Action. A week later Mini Shai-Hulud, appears on four SAP packages.
- TanStack Poisoned a GitHub Actions cache, scraped an OIDC token out of runner memory, and published through trusted publishing with valid provenance. Source released publicly the next day.
-
Miasma and Hades
Forks. Miasma executes from
binding.gypto dodge install-script controls and Hades runs on Pythonimport. - Here We Go Again keyv, cacheable, then 400+ packages on stolen tokens.
Introduction of the Worm
Shai-Hulud is the sandworm from Dune, the sci-fi novel by Frank Herbert turned movie franchise. Whoever wrote the first version chose the name themselves and put it in the payload, and every operator since has kept the branding: "Sha1-Hulud: The Second Coming", "Shai-Hulud: The Third Coming", "Shai-Hulud: Here We Go Again".
Generally, this is how the worm works:
- Land on a machine or a CI runner via a package install.
- Harvest every credential in reach.
- Find a credential that can publish, and use it to infiltrate that publisher's packages.
- Wait for someone to install those.
Wave 1: September 2025, Shai-Hulud
It started with @ctrl/[email protected], a package with over two million weekly
downloads, which went out carrying the payload. On September 16 researchers
found compromised packages published under CrowdStrike's npm publisher account.
Counts vary but 187 packages were affected in the first day of public reporting, and over 500 in the end.
Wave 2: November 2025, "The Second Coming"
Two months later it came back.
It got in through a malicious commit in asyncapi/cli. On the day,
Aikido Security
counted 492 packages with more than 132 million monthly downloads; the
final tally was 796 packages across 1,092 versions, with credentials
exfiltrated from over 500 GitHub users across 150-plus organizations.
Zapier, Postman, PostHog, ENS Domains and AsyncAPI packages all carried it.
This time, if it could not exfiltrate and could not propagate, it attempted to
overwrite the user's home directory.
Wave 3: April 2026, "The Third Coming" and the Mini
April 22 brought a wave carrying the string "Shai-Hulud: The Third Coming".
Its highest-leverage victim was @bitwarden/cli version 2026.4.0: a password
manager CLI, present across developer and DevOps tooling. Nobody stole a
Bitwarden npm token to do it. The attackers had already breached Checkmarx's
CI/CD and backdoored checkmarx/ast-github-action, an Action that Bitwarden's
release pipeline trusted. The malicious version was live for 93 minutes.
April 29 labelled Mini Shai-Hulud: a smaller, cleaner, more automated implementation of the same idea. Its first outing included four SAP npm packages carrying a two-stage credential stealer aimed at GitHub tokens, AWS keys and CI pipelines.
Wave 4: May 2026, TanStack and the OIDC problem
On May 11, in a six-minute window, 84 malicious versions across 42
@tanstack/* packages were published. @tanstack/react-router alone has over
twelve million weekly downloads. By the following day the count had reached 373
malicious versions across 169 packages, spreading into @uipath, @squawk,
@tallyui, Mistral AI and OpenSearch scopes, and the worm's full source was
published publicly.
The packages went out through trusted publishing carrying valid provenance attestations, which means the package came from a legitimate source, but it does not prove the package is safe.
The payload had become more thorough: it swept dozens of known credential locations, from GitHub and CircleCI to Kubernetes, Vault, SSH keys and crypto wallets, planted hooks in Claude Code and VS Code config so that opening a repository was enough to trigger it, and ran a daemon that wiped the home directory the moment it saw its tokens revoked.
Wave 5: June 2026, Miasma and Hades
Miasma, discovered on June 1, stopped using install scripts.
It shipped a 157-byte binding.gyp file, the manifest for node-gyp native
builds, and got execution through the native build step instead.
Install-script controls do not see it. It reached Red Hat's
@redhat-cloud-services packages, the @asyncapi namespace again, and 73
Microsoft-linked GitHub repositories.
Hades, was the PyPI branch. Its trigger is an obfuscated
one-line hook in a package's __init__.py, so it executes on import: no
install script, no build step, just import, with prompt injection aimed
at AI coding assistants reading the package source. Payloads were stashed in
GPG-encrypted form and the operators used underworld-themed repository names.
Wave 6: August 2026, keyv and cacheable
On August 4th the GitHub account of Jared Wray, maintainer of the
cacheable family, was taken over. The initial access vector has not been
published. Malicious commits went to main, and within minutes eleven packages
were affected:
keyv, cacheable, cacheable-request, cache-manager, flat-cache,
file-entry-cache, @keyv/*, @cacheable/utils, @cacheable/memory,
@cacheable/net, @cacheable/node-cache.
Known-bad versions include [email protected], [email protected],
[email protected], [email protected], @cacheable/[email protected],
[email protected] and [email protected]. The last clean releases are
[email protected], [email protected], [email protected],
[email protected] and [email protected].
400+ packages across 1,700+ versions
were affected. Each package gained a preinstall hook that downloads a
standalone Bun runtime to run the real stealer, sidestepping Node entirely. On
npm 12 and newer, preinstall does not run by default, so none of it executed.
What actually changed, wave to wave
| Execution trigger | Runtime | Publishing identity | |
|---|---|---|---|
| Sept 2025 | postinstall | Node | stolen npm token |
| Nov 2025 | preinstall | Bun | stolen npm token |
| Apr 2026 | preinstall | Bun | compromised CI, trusted Action |
| May 2026 | poisoned CI cache | Bun | OIDC, valid provenance |
| Jun 2026 | binding.gyp, Python import | Bun | stolen token |
| Aug 2026 | preinstall + IDE config | Bun | hijacked maintainer account |
What to do about it
Not a program. Things that actually blocked one of these waves.
-
Get to npm 12, or set
ignore-scripts. This is the single highest-value change available, and August proved it in production. -
Rotate in order of blast radius: npm publish tokens, GitHub credentials, cloud keys, Kubernetes and Vault, database credentials.
-
Inspect cloned repositories before opening them in an editor.
.vscode/and.claude/are executable surface now. -
Delay adoption. Most of these waves were live for hours to a day. A cooldown before you accept a new version costs you nothing and removes you from the window entirely. npm 11.10 and newer take a minimum release age in days, so set it once and forget it:
npm config set min-release-age 3Three days clears every window in this post.
-
Know what is actually on your disk. Not the repo you have open today: every project directory, every global install, every lockfile. That is the question every one of these advisories asks, and the one most teams cannot answer quickly.