Read this scroll as text
Full text of this interactive scroll.
- Threat
- Spread
- Detect
- Prevent
- History
- Resources
When the worm wears a trusted signature .
On May 11, 2026, a self-propagating worm dubbed Mini Shai-Hulud compromised 84 npm artifacts across 42 @tanstack/* packages in six minutes. Within forty-eight hours the campaign had reached 172 packages across npm and PyPI, totalling more than half a billion cumulative downloads. Every malicious version carried a valid SLSA Build Level 3 provenance attestation. The signatures were genuine. The code was not. This is how it worked, how to know if you were caught, and what to do next.
172 Packages hit 403 Malicious versions 518M Weekly download reach 6 min From publish to detection What it isAn identity attack dressed as a trusted release.
Mini Shai-Hulud is not a typosquat and it is not a hijacked maintainer account. It is a worm that hijacks the release pipeline itself, mints a short-lived publish token through the project's own OIDC binding, and ships a malicious version that the registry, the signer, and every downstream verifier all consider perfectly legitimate.
The class of attack
A software supply-chain worm: malware that uses the credentials it steals to infect more packages, which infect more developers, which yield more credentials. The propagation is automatic. No human attacker is in the loop after the initial foothold.
The "Mini" in the name refers to a TeamPCP variant first observed in April 2026 against SAP CAP packages. The May wave is its fifth and largest run since the original Shai-Hulud worm appeared in September 2025.
Why it matters
The compromised packages include @tanstack/react-router , which receives approximately 12 million weekly downloads. Other named victims include @uipath/* , @mistralai/* , @opensearch-project/* , and the Guardrails AI PyPI package.
CVE-2026-45321 was issued at CVSS 9.6. The TanStack family is the documented index case, but the worm spreads to anyone whose CI/CD secrets it can reach.
The three-link chain
01 · Pwn RequestA fork opens a draft PR
The TanStack bundle-size workflow ran on pull_request_target for fork PRs and checked out the fork's merge ref. Attacker code ran with the base repo's trust.
02 · Cache WedgePoison crosses the boundary
Attacker code wrote a malicious pnpm store into the GitHub Actions cache. When maintainer PRs were merged to main, the release workflow restored the poisoned cache.
03 · Memory ReadOIDC token leaves the runner
Attacker binaries read the runner's /proc/<pid>/mem and extracted the OIDC token. The token was used to mint a short-lived npm publish credential through trusted publishing.
04 · Signed ShipSigstore attests honestly
The build pipeline really did run inside tanstack/router on refs/heads/main . The attestation is correct. SLSA proves provenance, not authorisation.
The attacker did not invent novel tradecraft. They recombined published research. — TanStack postmortem, May 12 2026 Threat actorTeamPCP
Also tracked as DeadCatx3, PCPcat, ShellForce, CipherForce. Attributed by StepSecurity and Wiz with high confidence based on toolchain reuse from the SAP CAP, Bitwarden CLI, Lightning, Intercom and Trivy compromises.
Payload2.3 MB obfuscated stealer
A file named router_init.js with daemonisation, ten parallel credential-collection classes, and eleven AES-256-GCM encrypted sub-payloads decrypted only by the Bun runtime.
ExfiltrationThree redundant channels
The Session decentralised messenger network, GitHub dead-drop repositories, and a typosquat domain git-tanstack[.]com . There is no attacker-controlled C2 to block by IP.
How it spreadsOne compromised maintainer becomes a hundred.
A worm earns the name when it spreads without a human in the loop. Once Mini Shai-Hulud lands on a developer machine or a CI/CD runner, it looks at what the victim has the keys to and uses those keys to infect the next set of packages.
The propagation loop
What the worm steals
GitHub Actions OIDC tokens, GitLab and CircleCI runner tokens, AWS IMDSv2 role credentials ( 169.254.169.254 ), ECS/Fargate task metadata ( 169.254.170.2 ), GCP and Azure metadata, Kubernetes service accounts, HashiCorp Vault tokens on 127.0.0.1:8200 , npm and package-registry tokens, cryptocurrency wallets, password managers including 1Password and Bitwarden vault data, and credentials for messaging and AI tooling.
Where it hides
Persistence is dropped into ~/.claude/settings.json as a SessionStart hook, ~/.vscode/tasks.json as a folderOpen task, and ~/.claude/router_runtime.js as the staged payload. On macOS a LaunchAgent named com.user.gh-token-monitor polls GitHub every sixty seconds. On Linux the equivalent runs under systemd.
The dead-man switch
The gh-token-monitor daemon polls GitHub with the stolen token. If it receives an HTTP 40x indicating the token was revoked, it attempts to execute rm -rf ~/ on the host. The switch has a 24-hour TTL and exits cleanly afterwards, but if you act within that window you must disable persistence before rotating any tokens.
Are you compromised?The first hour matters more than the next ten.
If you installed any affected package between 2026-05-11 19:20 UTC and the deprecation window two to three hours later, treat the install host as compromised. The order of operations below is the consensus guidance from TanStack, StepSecurity, Snyk, Wiz, and Endor Labs.
Before you do anything else
Do not revoke npm or GitHub tokens before you have either disabled the dead-man switch or isolated and imaged the affected machine. The destructive handler is the first thing to neutralise.
Step 1 — Look for the payload on disk
/dev/null">Copy # The 2.3 MB payload is the fastest single check. $ find . -name 'router_init.js' -size +1M 2>/dev/null /dev/null">Copy # The orphan-commit SHA from optionalDependencies is a clean signal. $ grep -r '79ac49eedf774dd4b0cfa308722bc463cfe5885c' \ package-lock.json pnpm-lock.yaml yarn.lock 2>/dev/null /dev/null">Copy # Persistence artefacts in user tooling directories. $ ls -la ~/.claude/settings.json \ ~/.claude/router_runtime.js \ ~/.vscode/tasks.json 2>/dev/null Copy # Disable the dead-man switch before anything else. $ launchctl list | grep gh-token-monitor # macOS $ systemctl --user list-units | grep gh-token-monitor # LinuxStep 2 — Check your GitHub footprint
Copy # Stolen-credential dead-drop repos use this exact description. $ gh search repos --owner @me 'Shai-Hulud: Here We Go Again' Copy # The worm authors dead-drop commits as claude@users.noreply.github.com. $ gh search commits \ --author 'claude@users.noreply.github.com' \ --owner @meStep 3 — Run the interactive triage
Before you click, ask yourself honestly: when did your build last run, and would you know if a fresh tarball had been pulled into it? The questions below take thirty seconds.
Q1 Did your CI or any developer machine run npm install against an affected scope between 2026-05-11 19:20 UTC and 2026-05-12 02:00 UTC? Yes, or unsure Confirmed no Q2 Do your lockfiles pin integrity hashes for the affected packages, with hash verification enforced in CI? Yes, with mismatch blocking No, or partially Q3 Have you searched the affected hosts for router_init.js larger than 1 MB? Searched, nothing found Found a match Not yet searched Start overIndicators of compromise
| Type | Indicator |
|---|---|
| File | router_init.js — 2.3 MB obfuscated payload, sometimes renamed router_runtime.js |
| File | setup.mjs — Bun installer dropped under ~/.claude/ or ~/.vscode/ |
| Persistence | ~/.claude/settings.json with a SessionStart hook re-executing the payload |
| Persistence | ~/.vscode/tasks.json with a folderOpen task pointing at setup.mjs |
| Persistence | ~/Library/LaunchAgents/com.user.gh-token-monitor.plist (macOS) or equivalent systemd unit (Linux) |
| SHA | 79ac49eedf774dd4b0cfa308722bc463cfe5885c — orphan commit referenced by optionalDependencies |
| String | Shai-Hulud: Here We Go Again — description on public GitHub dead-drop repositories |
| String | A Mini Shai-Hulud has Appeared — description on worm-marker repositories |
| Commit author | claude@users.noreply.github.com on dead-drop commits |
| Branch name | Dune universe terms: atreides , fremen , harkonnen , melange , sietch , sardaukar , stillsuit , thumper , and others |
| Network | filev2[.]getsession[.]org , seed1[.]getsession[.]org – seed3[.]getsession[.]org |
| Network | git-tanstack[.]com (typosquat), api[.]masscan[.]cloud , 83[.]142[.]209[.]194 |
| CVE | CVE-2026-45321 , CVSS 9.6, GHSA-g7cv-rxg3-hmpx |
If any of these turn up, isolate the host, image it forensically, then rotate credentials in this order: npm tokens, GitHub personal access tokens, cloud keys (AWS, GCP, Azure), Vault tokens, Kubernetes service accounts, SSH keys.
PreventionThe control that closes each gap.
Provenance attestations and trusted publishing are still better than long-lived static tokens. Endor Labs' Peyton Kennedy put it bluntly to VentureBeat: the actual control that matters here is OIDC scope, not provenance and not 2FA. The checklist below works through the six gaps Mini Shai-Hulud exploited.
0 /9 controlsPin id-token: write to the publish job only OIDC
Set permissions: id-token: none at the workflow root and grant id-token: write only in the specific job that publishes. Trusted-publisher bindings should pin the workflow filename and the branch refs/heads/main .
Enforce lockfile integrity in CI SRI
Use Subresource Integrity or lockfile hash verification with pinned integrity fields for every dependency. Any version with a hash mismatch should block the build. This breaks the worm's quiet republish loop.
Set a minimum release age in every package manager Cooldown
Most malicious versions are detected and yanked within hours of publication. A three-day cooldown closes the smash-and-grab window entirely. Configure in .npmrc , pnpm-workspace.yaml , .yarnrc.yml , bunfig.toml , and uv.toml — copy-paste configs are in the panel below.
Remove pull_request_target from build workflows Trigger
The trigger is fine for labelling and comments. It is dangerous wherever it checks out the fork's merge ref and runs build commands. Use pull_request with a separate, scope-limited automation workflow.
Stop trusting the GitHub Actions cache across boundaries Cache
Treat the Actions cache as untrusted input when it is reachable from fork-triggered runs. Either disable cache restoration in release workflows or scope cache keys so the publish path can never restore a fork-poisoned blob.
Disable lifecycle scripts in CI by default npm
Run npm ci --ignore-scripts wherever practical. The payload depends on preinstall and prepare hooks; ignoring them removes the foothold even when a poisoned tarball reaches the runner.
Monitor for optionalDependencies referencing orphan commits Provenance
A reference to a SHA that is not on any branch is the smoking gun in this campaign. Provenance attestations remain a necessary signal — they are not a sufficient one. Treat them as part of the picture, not the whole picture.
Use ephemeral, short-lived secrets in build runners Identity
If the payload reads runner process memory and the only thing in memory is a token that expires in five minutes with narrow scope, the blast radius is small. Long-lived PATs and broad cloud admin roles on shared runners are the actual prize.
Subscribe to a feed and rehearse the response Process
Wire Socket, Snyk, StepSecurity or your registry's own advisory feed into your build alerts. Rehearse the order of operations: isolate first, disable persistence, image, then rotate. The dead-man switch makes muscle memory matter.
Minimum release age — copy-paste configs
Mini Shai-Hulud was detected six minutes after publish, and the affected versions were deprecated within hours. A three-day minimum-age policy in your package manager would have made every affected version unreachable. Each manager calls the feature something different and measures time in a different unit; the five configs below all enforce the same three-day cooldown.
npm pnpm Yarn Bun uv (Python) File ~/.npmrc · npm 11.10.0+ · unit: days Copy min-release-age=3Applies to both npm install and npm ci . Cannot be combined with --before in the same invocation — npm errors out if both are present. npm CLI 12 is expected to make a cooldown the default; setting it explicitly now keeps your config future-proof.
File pnpm-workspace.yaml · pnpm 10.16+ · unit: minutes Copy minimumReleaseAge: 4320 minimumReleaseAgeExclude: - '@yourorg/*'4320 minutes = 3 days. pnpm 11 turns this on by default at 1440 minutes (1 day). The minimumReleaseAgeExclude list takes glob patterns and exact locators like nx@21.6.5 — the place to keep your internal scope flowing while gating the public registry.
File .yarnrc.yml · Yarn 4.10.0+ · unit: duration string Copy npmMinimalAgeGate: "3d" npmPreapprovedPackages: - "@yourorg/*" enableScripts: falseYarn accepts duration strings like "3d" , "72h" , or numeric minutes — all equivalent. npmPreapprovedPackages accepts both globs and exact locators, more flexible than the other managers. enableScripts: false is the lifecycle-script equivalent of npm ci --ignore-scripts .
File bunfig.toml · Bun 1.3+ · unit: seconds Copy [install] minimumReleaseAge = 259200259200 seconds = 3 days. Bun disables lifecycle scripts by default, so the age gate is the main install-time control to add. Use --minimum-release-age 259200 on the CLI for one-off overrides. Worth noting: the Mini Shai-Hulud payload itself ran under Bun specifically to evade Node-process monitoring — a fact that does not change the value of using Bun for your own installs, but is darkly funny.
File ~/.config/uv/uv.toml · Python · unit: duration string Copy exclude-newer = "3 days"The May 2026 wave hit PyPI as well as npm ( guardrails-ai , mistralai , more). uv accepts relative durations natively. pip users can approximate this with pip install --uploaded-prior-to $(date -u -d '3 days ago' +%Y-%m-%dT%H:%M:%SZ) , but uv is the better target if you can move to it.
One gotcha — bots can still race the cooldown
The age gate is enforced at install time, not at update-suggestion time. If you use Renovate or Dependabot, configure their cooldown independently or they will keep opening PRs against versions your local install will refuse. Renovate's config:best-practices preset already sets a 3-day default for npm; Dependabot's equivalent is cooldown.default-days .
Once attackers gain access to publishing workflows and pipeline identities, the software delivery process itself becomes the distribution mechanism. — Avital Harel, Upwind, May 12 2026 The arc of the wormFrom a graduate-student error to a CI/CD distribution mechanism.
Self-replicating malware is older than the World Wide Web. Each generation has run on the era's most-trusted plumbing. The Internet itself in 1988. Email in the early 2000s. Now the package registries that every developer has open in another tab.
November 2, 1988The Morris Worm
At around 8:30 p.m. on November 2, a Cornell graduate student named Robert Tappan Morris released a self-replicating program from an MIT computer. Within twenty-four hours roughly 6,000 of the 60,000 machines then connected to the Internet had been hit. Morris exploited a backdoor in sendmail, a buffer overflow in the finger daemon, and weak passwords. A design choice to re-infect 14% of the time regardless of an already-infected response turned a thought experiment into a denial-of-service event.
Result: first conviction under the 1986 Computer Fraud and Abuse Act · first CERT created in Pittsburgh days later · the first time The New York Times used the phrase "the Internet" in print
Late August 2025The Nx compromise
An attack on the Nx build-system maintainers established the playbook the Shai-Hulud family would later extend: harvest credentials from infected developer machines, exfiltrate via the victim's own GitHub account, and weaponise AI command-line tools for reconnaissance. The Nx campaign was the first widely-noted use of AI tooling inside the malware's own execution loop.
September 14, 2025Shai-Hulud appears
ReversingLabs detected a self-replicating worm in the npm registry, naming it after Frank Herbert's Dune sandworms. Patient zero was rxnt-authentication@0.0.3 , published at 17:58:50 UTC on September 14. The worm enumerated each compromised developer's packages, injected itself, and republished. By disclosure at least 187 packages were confirmed infected. Stolen credentials were dumped into newly created public repositories named Shai-Hulud on each victim's account.
November 24, 2025Shai-Hulud 2.0 — The Second Coming
Datadog Security Labs documented a second wave dubbed Sha1-Hulud: The Second Coming, affecting 796 unique npm packages and over twenty million weekly downloads. The 2.0 variant shifted execution from postinstall to preinstall, switched to running under the Bun runtime to evade Node.js monitoring, and introduced a more aggressive home-directory destruction fallback. Data from at least 500 GitHub users in 150 organisations was successfully exfiltrated.
December 2025Shai-Hulud 3.0
A third variant identified in @vietmoney/react-big-calendar@0.26.2 . The mechanics were largely unchanged; the obfuscation and persistence were tightened. The campaign settled into a recognisable shape: a self-replicating credential stealer driven by install-time hooks.
March 2026TeamPCP attribution emerges
StepSecurity attributed the Aqua Security Trivy scanner compromise (March) and the Bitwarden CLI npm package compromise (April) to a single actor cluster they named TeamPCP. The group reused the Shai-Hulud worm toolchain but tuned it for higher-value targets and more careful access vectors.
Late April 2026Mini Shai-Hulud at SAP
The first Mini Shai-Hulud variant compromised four SAP CAP framework npm packages including @cap-js/sqlite . The intercom-client SDK fell twenty-nine hours later via stolen CI/CD infrastructure from that initial wave. The campaign demonstrated identity-driven propagation through trusted CI/CD pipelines, rather than direct token theft.
May 11, 2026Mini Shai-Hulud — Here We Go Again
Between 19:20 and 19:26 UTC, the worm published 84 malicious versions across 42 @tanstack/* packages. Within forty-eight hours the campaign reached 172 packages and 403 malicious versions across npm and PyPI. Microsoft's analysis of the malicious mistralai PyPI package found country-aware logic that avoids Russian-language environments and a geofenced destructive branch with a one-in-six chance of executing rm -rf / when the system appears to be in Israel or Iran.
First documented npm worm to produce validly-attested malicious packages · CVE-2026-45321 · CVSS 9.6
May 12, 2026 — What followsThe worm goes open-source
Late on May 12, the malware research collective vx-underground reported that the fully weaponised Shai-Hulud worm code had been open-sourced. If confirmed, the attack is no longer limited to TeamPCP. Any actor with a target list and a misconfigured CI/CD pipeline can now reproduce the cache-poisoning, OIDC-extraction, and provenance-attested publishing chain.
The Morris Worm was a wake-up call for the country and the cyber age to come. Each generation since has had its own. — After the FBI, on the 1988 attack Read furtherPrimary sources, advisories, and detection tooling.
The links below are the source material for everything above. The technical analyses from TanStack, StepSecurity, Socket, Snyk, and Wiz are the most detailed; the FBI's Morris account and ReversingLabs' original Shai-Hulud post are the historical anchor.
PostmortemTanStack npm supply-chain compromise
The maintainers' own timeline, three-vulnerability breakdown, and follow-up questions. The most useful single document if you want to understand exactly how the chain bridged each trust boundary.
Read on tanstack.com Threat analysisStepSecurity — Mini Shai-Hulud Is Back
Ashish Kurmi's analysis from the team that filed GitHub issue #7383 twenty minutes after publish. Includes the full IOC list, Dune branch-name vocabulary, and worm-marker repository names.
Read on stepsecurity.io Threat analysisSocket — TanStack npm packages compromised
The Socket Threat Research team's analysis with a sortable table of compromised package artefacts across all Mini Shai-Hulud waves, including Mistral AI, UiPath, TanStack, and others.
Read on socket.dev Threat analysisWiz — Mini Shai-Hulud strikes again
Detailed walkthrough of the chained GitHub Actions exploitation, the three exfiltration channels, the gh-token-monitor daemon behaviour, and the attribution to TeamPCP.
Read on wiz.io Threat analysisSnyk — TanStack npm packages hit by Mini Shai-Hulud
The CVE record, the wider history of Shai-Hulud waves, and clear notes on why valid SLSA provenance does not equal authorisation. Links to community-maintained detection scripts.
Read on snyk.io Threat analysisOX Security — 170+ packages hit across npm & PyPI
Scope, impact, and the count of dead-drop repositories created during the May wave. Notes on the Python infection vector via the __init__.py file in guardrails-ai and mistralai .
Read on ox.security Threat analysisUpGuard — The Shai-Hulud attack explained
A walkthrough of the original September 2025 worm: the precursor phishing campaign, the rxnt-authentication patient-zero package, public dead-drop repositories, and the malicious GitHub Actions workflow file.
Read on upguard.com Threat analysisJFrog — Shai-Hulud: Here We Go Again
JFrog Security Research's analysis covering the worm-like behaviour, the destructive dead-man switch, and the deduplicated list of campaign infrastructure to block at the network edge.
Read on jfrog.com HistoryFBI — The Morris Worm
The Bureau's account of the November 1988 incident: the timeline, the trial, the founding of the first CERT in Pittsburgh, and the genuine impact on a country just learning what vulnerability to a network actually meant.
Read on fbi.gov ReferenceReversingLabs — Shai-Hulud FAQ
A plain-language FAQ for engineers. What was the worm. How did it spread. What was a Shai-Hulud Migration. What did you need to check on your own GitHub and your own packages.
Read on reversinglabs.com DetectionDatadog — Shai-Hulud 2.0 IOCs
Datadog Security Labs' GitHub repository with a deduplicated list of indicators of compromise spanning seven vendors. The most practical single feed if you need machine-readable IOCs for the Shai-Hulud family.
View on github.com DetectionCommunity scanners
Community-maintained tools referenced by Snyk: GLPMC/Tanstack-Worm-Detector and omarpr/mini-shai-hulud-ioc-scanner. Useful as a quick second opinion. Always verify before running.
Search github.com Stay currentThe next wave is already being written. Subscribe to a feed, not a memory.
Every Shai-Hulud variant since September 2025 has reused tradecraft from the one before it. The mitigations stay the same; the access vectors get more interesting. Wire a real advisory feed into your build alerts and rehearse the response.
Read Socket's full analysis Discuss with a defender Field Guide v1.0 · Compiled May 13, 2026 Sources: TanStack, StepSecurity, Socket, Snyk, Wiz, OX Security, JFrog, FBI