Skip to content
guide

Mini Shai-Hulud npm worm: detect, rotate, and shrink your blast radius

| 8 min read

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/detect and 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-scripts by 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.