Mini Shai-Hulud npm worm: detect, rotate, and shrink your blast radius
On May 11, 2026, between 19:20 and 19:26 UTC, the TeamPCP threat actor pushed over 400 malicious versions of 170+ npm and PyPI packages in a single coordinated burst. TanStack Router, TanStack Query adapters, Mistral AI SDK, UiPath, and Guardrails AI were all in the blast radius. Six minutes of publishing; weeks of cleanup.
This wave is the Mini Shai-Hulud worm: a self-spreading post-install payload that steals maintainer tokens, then uses those tokens to publish more poisoned versions across the packages the victim controls. OpenAI asked macOS users to update. CI pipelines around the world ran the payload before anyone read the advisory.
If you ran npm install on a CI runner or developer laptop after 2026-05-11 19:20 UTC,
you owe yourself a 60-second check, a rotation pass, and a structural change so the next wave
costs you less. Here is the playbook.
Step 1: detect in under 60 seconds
The worm leaves three loud signals: compromised package versions in your lockfile, a Git commit
or repository named Shai-Hulud-Migration, and unexpected GitHub Actions runs that
exfiltrate secrets to webhook.site or a TruffleHog binary. Run this from any host
with gh and npm installed:
# 1. Did any compromised package land in your lockfile after May 11?
COMPROMISED="@tanstack/react-router @tanstack/router-core @mistralai/mistralai @uipath/activities guardrails-ai"
for pkg in $COMPROMISED; do
npm ls "$pkg" 2>/dev/null | grep -v empty && echo "REVIEW: $pkg"
done
# 2. Look for the worm's calling card in your repos
gh api -X GET search/repositories \
-f q="org:$YOUR_ORG Shai-Hulud-Migration in:name" \
--jq '.items[].full_name'
# 3. Scan recent GitHub Actions runs for unknown workflows
gh run list --limit 200 --json name,createdAt,event,headBranch \
| jq '.[] | select(.createdAt > "2026-05-11T19:00:00Z")'
# 4. Audit npm token list, then rotate every one of them
npm token list
npm token revoke <token-id> Any hit on any one of those four commands means you skip to step 2 immediately. There is no "look into it first" path. The worm finishes before the advisory does.
The Mini Shai-Hulud payload runs in your postinstall hook with the privileges of
whoever ran npm install. On a CI runner with OIDC, that is a federated identity
with cloud access. On a developer laptop, that is every secret in ~/.aws,
~/.ssh, ~/.npmrc, and your shell history.
Step 2: rotate first, investigate second
Forensics is for next week. Rotation is for the next minute. Open a runbook tab for every secret surface the affected machine touched and revoke in parallel. This script enumerates the surfaces so nothing gets missed:
// Rotate-first script. Run this BEFORE you finish reading the advisory.
// Lists every key the runner touched, deletes it, and re-issues from a vault.
import { execSync } from "node:child_process";
const surfaces = [
{ name: "npm", cmd: "npm token list" },
{ name: "gh-pat", cmd: "gh auth status" },
{ name: "aws", cmd: "aws iam list-access-keys" },
{ name: "gcp", cmd: "gcloud iam service-accounts keys list --iam-account=$SA" },
{ name: "stripe", cmd: "stripe api_keys list" },
];
for (const s of surfaces) {
console.log(`[${s.name}] enumerate`);
try {
console.log(execSync(s.cmd, { encoding: "utf8" }));
} catch (e) {
console.warn(`skip ${s.name}: not configured on this host`);
}
}
// Rotation itself is per-surface; pair this with your secrets manager.
// The point: have the list in front of you in under 60 seconds. The rotation order that catches the worm's downstream moves is: npm tokens (stops new publishes), GitHub PATs (stops repository creation), cloud IAM keys (stops data exfil), third-party SaaS keys (Stripe, Slack, OpenAI, Anthropic), and finally SSH keys cached on disk. Update your secrets manager only after the old credentials are dead at the source.
Use the breach API to check if your CI email leaked
TeamPCP publishes IOC dumps that include maintainer emails harvested from compromised
~/.npmrc files. Run a breach check against your release bot accounts to confirm
whether your address appeared in the May 12 dumps:
curl -X POST https://api.botoi.com/v1/breach/check \
-H "Content-Type: application/json" \
-d '{"email": "release-bot@acme-corp.com"}' {
"data": {
"email": "release-bot@acme-corp.com",
"breached": true,
"breach_count": 3,
"latest_breach": "2026-05-12",
"sources": ["npm-token-leak-2026-05", "github-pat-dump", "TeamPCP-IOC-list"]
}
} If your release email shows up in the latest source, the corresponding npm token is already public. Revoke it, rotate the publish workflow's OIDC trust relationship, and re-issue under a new identity.
Step 3: scrub leaked secrets from logs and tickets
Worm payloads dump environment variables into logs that flow into Datadog, Sentry, Slack, and your ticketing system. Treat every log line written after 19:20 UTC on May 11 as suspect. Pipe high-risk fields through a PII and secret detector before they hit cold storage:
curl -X POST https://api.botoi.com/v1/pii/detect \
-H "Content-Type: application/json" \
-d '{"text": "AWS_ACCESS_KEY_ID=AKIA4XQ... npm_token=npm_a1b2c3..."}' {
"data": {
"found": true,
"matches": [
{ "type": "aws_access_key", "value": "AKIA4XQ...", "start": 18, "end": 38 },
{ "type": "npm_token", "value": "npm_a1b2c3...", "start": 49, "end": 65 }
]
}
} The same endpoint flags AWS keys, npm tokens, GitHub PATs, Stripe keys, and JWTs. Wire it into a log middleware that fails closed: if the detection call times out, drop the line rather than write a potentially poisoned record. Better to lose a log entry than to ship credentials to a queryable index.
Step 4: block the next wave with a CI guard
The worm relies on three things landing on your runner: a compromised package version, a post-install hook that runs, and outbound network access to publish. Block any one of them and the chain breaks. This GitHub Actions check fails the PR if a lockfile update introduces a known-bad version, a worm IOC, or a runtime dep without signed provenance:
name: block-worm-iocs
on: [pull_request]
jobs:
scan:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Fail on Shai-Hulud markers
run: |
! grep -rE "Shai-Hulud|TruffleHog|webhook\.site" .github/ package.json package-lock.json
- name: Block compromised package versions
run: |
BAD=$(node -e "
const lock = require('./package-lock.json');
const bad = [];
for (const [k, v] of Object.entries(lock.packages || {})) {
if (k.includes('@tanstack/') && v.version && v.resolved?.includes('npm.pkg.github.com') === false) {
// pin against advisory list
}
}
console.log(bad.join('\n'));
")
[ -z "$BAD" ] || (echo "BLOCKED: $BAD"; exit 1)
- name: Require provenance for runtime deps
run: npm audit signatures --omit=dev
Pair it with npm config set ignore-scripts true on CI runners and an explicit
allowlist for packages that legitimately need a build step (TypeScript, esbuild, Sharp). The
one-time noise is worth the worm-proof default.
Step 5: shrink the surface that can host a worm
The most-poisoned package types across the April and May 2026 waves were AI SDKs, validation helpers, HTTP clients, and tiny utilities. Every one of those is a candidate to delete and replace with a revocable HTTP call. Compare the blast radius:
| Category | Typical npm victim | HTTP replacement | Revocation |
|---|---|---|---|
| Email validation | validator, deep-email-validator | /v1/email/validate | Rotate API key |
| Breach check | hibp client packages | /v1/breach/check | Rotate API key |
| PII and secret scan | secretlint, pii-detector | /v1/pii/detect | Rotate API key |
| Phone parsing | libphonenumber-js | /v1/phone | Rotate API key |
Pick the packages with post-install hooks first. Pick the AI SDKs second (Mistral, Guardrails, and LiteLLM have all been compromised in the past two months). Keep heavy in-process libraries you actually need; delete the ones that wrap a 200ms call you could make yourself.
Key takeaways
- Detect in 60 seconds. Compromised package versions, repos named
Shai-Hulud-Migration, and unauthorized GitHub Actions runs are the three loud signals. One hit means rotate. - Rotate before you investigate. npm tokens, GitHub PATs, cloud keys, SaaS keys, SSH keys. The worm's downstream sale of credentials starts in minutes, not days.
- Scrub logs. Pipe high-risk fields through
/v1/pii/detectand fail closed on timeout. A missing log line beats a leaked credential. - Block the next wave. CI guard that rejects known-bad versions, worm IOCs,
and unsigned runtime deps. Pair with
ignore-scriptsby default. - Shrink the surface. Replace single-purpose npm packages with revocable HTTP calls. A breached API key dies in one request; a breached package already ran.
Botoi exposes /v1/breach/check, /v1/pii/detect,
/v1/email/validate, and roughly 200 other single-purpose endpoints behind one API
key with 5 req/min free. Wire them into your incident runbook or connect the
MCP server to
Claude Code so your AI assistant runs the detection script for you. Browse the
interactive docs
to start.
Frequently asked questions
- What is the Mini Shai-Hulud worm?
- Mini Shai-Hulud is a self-spreading npm and PyPI supply chain attack run by the TeamPCP threat actor. It compromises maintainer tokens, publishes malicious post-install code, then uses the stolen tokens to publish more poisoned versions across packages the maintainer owns. The May 11, 2026 wave hit TanStack, Mistral AI SDK, UiPath, and Guardrails AI; over 400 malicious versions of 170+ packages were published in roughly six minutes between 19:20 and 19:26 UTC.
- Which packages were affected on May 11, 2026?
- TanStack Router, TanStack Query (multiple framework adapters), Mistral AI SDK, UiPath npm packages, Guardrails AI, and several smaller libraries downstream of those maintainers. Wiz, Snyk, and StepSecurity all published advisories on May 12 with full IOC lists. If your lockfile pulled any version of those packages published after 2026-05-11 19:20 UTC, treat the CI host and developer machine as compromised.
- How do I tell if my CI was hit?
- Check three signals: a git commit named 'Shai-Hulud' pushed from a CI runner, an outbound request to webhook.site or a TruffleHog binary download in your CI logs, and any GitHub repository named 'Shai-Hulud-Migration' in your org. The worm exfiltrates secrets via GitHub Actions, so the audit log is your fastest detection surface. Filter for new workflow runs you did not author in the past 7 days.
- What should I rotate after exposure?
- Every secret the affected runner or laptop touched. npm tokens, GitHub PATs, AWS keys, GCP service accounts, Stripe live keys, Slack bot tokens, OpenAI and Anthropic API keys, internal database credentials, and any SSH keys cached on the machine. Do not investigate first; rotate, then investigate. Exfiltration completes in seconds; rotation closes the door before the stolen credentials are sold.
- How do single-purpose HTTP APIs reduce this risk?
- An HTTPS call to a single-purpose API replaces a transitively trusted package and its post-install hook with a revocable key. If the API provider is breached, you rotate one key and traffic stops. If a package in your tree is breached, the malware already ran on every machine that ran npm install. The trade is small latency for a tiny blast radius and instant revocation.
Try this API
Breach Check API — interactive playground and code examples
More guide posts
Start building with botoi
150+ API endpoints for lookup, text processing, image generation, and developer utilities. Free tier, no credit card.