Read this scroll as text

Full text of this interactive scroll.

Skip to main content
  • Threat
  • Spread
  • Detect
  • Prevent
  • History
  • Resources
LT DK 170+ Active campaign Field Guide · v1.0 For npm and PyPI defenders

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 is

An 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 Request
A 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 Wedge
Poison 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 Read
OIDC 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 Ship
Sigstore 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 actor
TeamPCP

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.

Payload
2.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.

Exfiltration
Three 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 spreads

One 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 # Linux
Step 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 @me
Step 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 over
Indicators of compromise
TypeIndicator
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 nameDune 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.

Prevention

The 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 controls
Pin 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=3

Applies 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: false

Yarn 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 = 259200

259200 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 worm

From 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, 1988
The 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 2025
The 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, 2025
Shai-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, 2025
Shai-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 2025
Shai-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 2026
TeamPCP 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 2026
Mini 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, 2026
Mini 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 follows
The 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 further

Primary 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.

Postmortem
TanStack 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 analysis
StepSecurity — 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 analysis
Socket — 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 analysis
Wiz — 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 analysis
Snyk — 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 analysis
OX 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 analysis
UpGuard — 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 analysis
JFrog — 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 History
FBI — 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 Reference
ReversingLabs — 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 Detection
Datadog — 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 Detection
Community 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 current

The 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
DID YOU HAVE A GOOD SCROLL?
Let us know what you think, we'd love to hear from you.