Overview

Who this is for. Developers using npm who want to know whether they were affected by the Mini Shai-Hulud supply-chain attack (CVE-2026-45321, CVSS 9.6). This runbook covers detection, safe remediation, and hardening. Security researchers will find a technical deep-dive in §5.

What to do first. Run the one-click detector before reading anything else:

curl -fsSL https://gist.githubusercontent.com/prashanthnatraj/6405cf30127b74b185e049dd2fd746e6/raw/shai-hulud-detector.sh | bash

The script is read-only, makes no network calls after download, and exits with code 0 (CLEAN), 1 (SUSPICIOUS), or 2 (INFECTED). Jump to §4 for what to do with each result.

If you take away one thing from this document:
Do not revoke any credentials before removing the persistence daemon. The daemon watches for revocation and triggers rm -rf ~. The correct order is detect → isolate → remove daemon → rotate. See §3.


§1. What the Attack Does

Mini Shai-Hulud is a supply-chain attack attributed to the threat actor TeamPCP. Between April 29 and May 12, 2026, the group injected malicious code into 170+ npm packages across several namespaces — most visibly TanStack, but also SAP cap-js, UiPath, Mistral AI, GuardRails AI, and OpenSearch. In total, 404 malicious package versions were published, accounting for an estimated 518 million download-installs before disclosure.

The attack follows a two-stage pattern. In stage one, the malicious postinstall script drops two payload files (router_init.js and tanstack_runner.js) to disk and installs a persistence daemon named gh-token-monitor. The daemon runs as a macOS LaunchAgent or Linux systemd user unit. In stage two, the daemon exfiltrates GitHub personal access tokens, npm publish tokens, and environment variables from the developer's machine. The exfiltrated GitHub token grants the attacker write access to any repository the victim has access to, enabling the attack to propagate further through CI/CD pipelines.

The most dangerous property of this attack is the wipe trigger. The npm token the worm installs is explicitly labeled IfYouRevokeThisTokenItWillWipeTheComputerOfTheOwner. This is not a bluff: revoking the GitHub or npm token while gh-token-monitor is still running causes the daemon to execute rm -rf ~, destroying the home directory. This makes standard incident response (revoke first, contain second) catastrophically wrong for this specific threat.


§2. Detection

Run the automated detector first (command above). The script performs seven checks in sequence. This section describes what each check looks for and what a positive result means.

2.1 Lockfile presence

The detector needs a lockfile (package-lock.json, pnpm-lock.yaml, or yarn.lock) to run dependency checks. If none is found, the scan returns SUSPICIOUS with a note that the dependency tree cannot be verified. This is not an infection indicator on its own.

2.2 @tanstack/setup — the single fastest tell

@tanstack/setup does not exist in the legitimate TanStack GitHub organization. Any reference to this package in package.json, package-lock.json, or node_modules is a confirmed infection indicator. No further investigation is needed to establish that the malicious package ran.

# Manual check (fastest single command):
grep -r "@tanstack/setup" package.json package-lock.json pnpm-lock.yaml 2>/dev/null

Empty output: not present. Any output: go to §3 immediately before doing anything else.

2.3 Known malicious namespaces

The CVE-2026-45321 advisory identifies six package namespaces that contained compromised /setup variants:

| Namespace | Legitimate use | Malicious variant | |---|---|---| | @tanstack | React table, query, router | @tanstack/setup | | @uipath | RPA automation SDK | @uipath/setup | | @mistralai | Mistral API client | @mistralai/setup | | @guardrails-ai | LLM output validation | @guardrails-ai/setup | | @opensearch-project | OpenSearch client | @opensearch-project/setup | | @cap-js | SAP Cloud Application Programming Model | @cap-js/setup |

Legitimate packages from these namespaces are not malicious. Only the /setup variant is an IOC. The detector flags /setup presence as INFECTED and broader namespace presence as SUSPICIOUS (warranting manual version verification).

2.4 Payload files on disk

The worm drops two files. Their presence confirms the postinstall script ran; their SHA-256 hash confirms they are the malicious versions:

| Filename | SHA-256 | |---|---| | router_init.js | ab4fcadaec49c03278063dd269ea5eef82d24f2124a8e15d7b90f2fa8601266c | | tanstack_runner.js | 2ec78d556d696e208927cc503d48e4b5eb56b31abc2870c2ed2e98d6be27fc96 |

# Manual check (macOS/Linux):
find ~ -maxdepth 10 \( -name "router_init.js" -o -name "tanstack_runner.js" \) 2>/dev/null

If found, compare the hash before concluding infection (the filename alone could be coincidental):

# macOS:
shasum -a 256 /path/to/router_init.js

# Linux:
sha256sum /path/to/router_init.js

2.5 Persistence daemon

# macOS — check for LaunchAgent plist:
ls ~/Library/LaunchAgents/ | grep -i token

# macOS — check if the service is running:
launchctl list | grep -i gh-token

# Linux:
systemctl --user list-units | grep -i gh-token

# Cross-platform process check:
pgrep -f "gh-token-monitor"

Any hit here is a confirmed infection indicator. Do not rotate credentials until the daemon is removed. See §3.

2.6 Claude Code hook injection

The worm modifies ~/.claude/settings.json to inject PreToolUse and PostToolUse hooks. These hooks can exfiltrate prompts or silently modify tool behavior during Claude Code sessions.

# Check for unexpected hooks:
cat ~/.claude/settings.json | grep -E "PreToolUse|PostToolUse"

The detector flags any hook entries as SUSPICIOUS because it cannot distinguish hooks you added from injected ones. Verify manually against your own settings history.

2.7 GitHub Actions workflow tampering

The worm's CI component modifies .github/workflows to add pull_request_target triggers and unpin action references. Both are supply-chain vectors.

# Check for pull_request_target in workflows:
grep -rl "pull_request_target" .github/workflows/ 2>/dev/null

# Check for actions not pinned to a full commit SHA:
grep -rh "uses:" .github/workflows/ 2>/dev/null | grep -v "@[0-9a-f]\{40\}"

§3. The Detect-Before-Rotate Rule

WARNING — READ THIS BEFORE REVOKING ANY CREDENTIAL

The gh-token-monitor daemon actively watches for GitHub and npm token revocation. When it detects revocation, it executes rm -rf ~, destroying the entire home directory. The malicious npm token is labeled IfYouRevokeThisTokenItWillWipeTheComputerOfTheOwner. This is the worm author's explicit statement of intent, not a warning about unintended side effects.

The correct remediation sequence is:

  1. Disconnect network
  2. Remove the daemon
  3. Remove payload files
  4. Reconnect
  5. Rotate credentials

Never rotate first. The instinct to revoke tokens immediately as a first response is correct for most incidents. It is wrong here. A 60-second deviation from this order risks permanent data loss that no credential rotation can fix.

This rule applies even if you are uncertain whether you are infected. If there is any ambiguity, run the daemon check (§2.5) before touching any token.


§4. Remediation

4.1 If INFECTED (detector exits 2)

Follow these steps in order. Do not skip or reorder.

Step 1 — Disconnect network immediately

Wi-Fi off. Ethernet unplugged. The daemon cannot exfiltrate further or receive a remote kill/update signal while offline.

Step 2 — Do not run npm, git, or shell package commands

Specifically: no npm install, npm uninstall, git push, or package manager equivalents. Postinstall scripts can re-trigger on package operations.

Step 3 — Remove the daemon

macOS:

# Find the exact plist name first:
ls ~/Library/LaunchAgents/ | grep -i token

# Unload and remove it:
launchctl unload ~/Library/LaunchAgents/<exact-name>.plist
rm ~/Library/LaunchAgents/<exact-name>.plist

Linux:

systemctl --user stop gh-token-monitor.service
systemctl --user disable gh-token-monitor.service
rm ~/.config/systemd/user/gh-token-monitor.service

Cross-platform verification (daemon should no longer appear):

pgrep -f "gh-token-monitor"
# Expected: no output

Step 4 — Remove payload files

find ~ \( -name "router_init.js" -o -name "tanstack_runner.js" \) -delete 2>/dev/null

Step 5 — Reconnect network and rotate credentials

Rotate in blast-radius order. Each rotation also requires updating .env.local and any hosted environment variable stores (Vercel, Netlify, etc.).

  1. GitHub PAT → github.com/settings/tokens — revoke all, reissue with fine-grained scopes only
  2. npm publish tokennpm token list then npm token revoke <id> — reissue with 2FA on publish required
  3. Vercel tokens → vercel.com/account/tokens — revoke and reissue, then audit deployment history for unauthorized deploys, redeploy from clean main
  4. Supabase service role key → Supabase dashboard → Settings → API — reissue, then audit:
    SELECT * FROM auth.audit_log_entries ORDER BY created_at DESC LIMIT 100;
    
  5. Cloudflare API tokens → dash.cloudflare.com → My Profile → API Tokens — audit log first (check for DNS record modifications), then rotate
  6. Stripe → dashboard.stripe.com/apikeys — rotate restricted and secret keys, audit webhook endpoints for unauthorized additions
  7. AI provider keys (Gemini, OpenAI, Anthropic, OpenRouter) — lower blast radius; rotate after the above
  8. Third-party integrations (Meta, Twitter/X, DataForSEO, etc.) — rotate last; check connected app permissions

Step 6 — Clean and reinstall dependencies

rm -rf node_modules package-lock.json
npm install --ignore-scripts

The --ignore-scripts flag prevents postinstall scripts from running during reinstall.

Step 7 — GitHub and Vercel audit

Check for unauthorized artifacts created using the stolen token:

# Check for dead-drop repositories:
gh repo list --limit 100 | grep -iE "shai-hulud|mini-shai|harkonnen|atreides|sandworm"

# Check for Dune-themed branches (the worm uses them as C2 references):
gh api /repos/<owner>/<repo>/branches \
  | grep -iE "atreides|fremen|harkonnen|sardaukar|melange|mentat|sandworm|thumper|sietch"

# Audit workflow files for recent unauthorized changes:
git log --oneline -- .github/workflows/

In the GitHub web UI: review Settings → Webhooks, Deploy Keys, and Collaborators. In Vercel: review Environment Variables for additions you did not make, and Team Members.

4.2 If SUSPICIOUS (detector exits 1)

A SUSPICIOUS result means one or more checks returned a warning but nothing is confirmed malicious. Common causes:

  • Legitimate packages in the affected namespaces (@tanstack/query, @mistralai/client-ts, etc.)
  • Workflow files using pull_request_target for legitimate fork PR workflows
  • Claude Code hooks you added yourself

For each SUSPICIOUS finding from the scan output, investigate manually before concluding clean or infected. Community scanners provide independent confirmation:

  • StepSecurity scanner — most comprehensive; catches the wipe trigger specifically
  • github.com/Cobenian/shai-hulud-detect — purpose-built for this attack
  • github.com/omarpr/mini-shai-hulud-ioc-scanner — IOC-focused

Run any of these while still offline (or with credentials not yet rotated). Do not run them after rotating, as the wipe trigger check requires the daemon to still be present to detect.

If any community scanner upgrades your result to INFECTED, follow §4.1 from Step 1.

4.3 If CLEAN (detector exits 0)

No action required for remediation. The hardening steps in §4.4 apply regardless.

You received this runbook either proactively or because something in your environment raised concern. A CLEAN result means the seven IOC categories checked by the detector are not present. It does not guarantee that no other attack vector was used against your stack.

4.4 Hardening (apply regardless of scan result)

Add to .npmrc at your project root:

ignore-scripts=true
minimum-release-age=4320

ignore-scripts=true blocks postinstall scripts during npm install. This neutralizes the primary vector for this class of attack. Note: some packages use postinstall scripts for legitimate purposes (native bindings compilation, etc.); you may need to selectively re-enable for specific packages.

minimum-release-age=4320 blocks packages published within the last 3 days (4320 minutes). The average detection window for supply-chain attacks of this type is 48–72 hours. This does not protect against slow-burn campaigns but provides meaningful immunity for fast-moving attacks like this one.

Additional hardening:

# Enable npm 2FA with hardware key (WebAuthn is stronger than TOTP):
npm profile enable-2fa auth-and-writes

In GitHub Actions, pin every uses: to a full 40-character commit SHA:

# Before (vulnerable):
- uses: actions/checkout@v4

# After (hardened):
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683  # v4.2.2

Remove every pull_request_target trigger that is not strictly required. If you need to handle fork PRs, use the safer pull_request trigger with explicit permission grants. The StepSecurity Harden-Runner action (free for public repos) provides runtime enforcement.

Subscribe to supply-chain scanner alerts: socket.dev, aikido.dev, or snyk.io all flagged the IOC packages within hours of disclosure. Any of them on free tier provides meaningful early warning.


§5. Technical Deep-Dive

This section is for security researchers and engineers interested in the attack mechanics. Skip to §6 if you just need to know whether you are infected.

SLSA Build Level 3 provenance spoofing

SLSA (Supply-chain Levels for Software Artifacts) Build Level 3 requires that provenance attestations be generated by a hosted build platform (GitHub Actions) in an isolated, ephemeral environment, with no possibility of modification by the package owner after the build completes. The TanStack packages compromised in this attack carried valid SLSA BL3 provenance — cryptographically signed attestations that the packages were built by the GitHub Actions runner, using the public repository source.

TeamPCP did not forge provenance signatures. Instead, they poisoned the build environment itself before provenance generation occurred. By the time the SLSA attestation was created and signed, the malicious code was already present in the build artifact. The signature accurately attests to the provenance of a malicious build.

pull_request_target abuse

GitHub's pull_request_target trigger runs with write permissions to the base repository, even when triggered by a PR from a fork. This is by design: it allows fork contributors to run workflows that can post PR comments or update status checks without requiring maintainer approval on every run.

TeamPCP exploited this by submitting PRs to targeted repositories that modified the workflow files to use pull_request_target and install a malicious cache key. On the next build, the cache restore step loaded the poisoned cache into the build environment.

OIDC token exfiltration from runner memory

GitHub Actions supports OpenID Connect (OIDC) tokens as short-lived credentials for authenticating to cloud providers. These tokens are scoped to the specific workflow run and expire after 5–10 minutes. The poisoned build environment used gettaskidtoken to extract the OIDC token from runner memory and exfiltrate it to a TeamPCP-controlled endpoint before the token expired. This token was then exchanged for a longer-lived cloud credential, providing persistent access to the compromised organization's cloud resources.

gh-token-monitor persistence design

The persistence daemon is designed around a specific assumption: that the developer's first response to discovering a breach is to revoke credentials. The daemon watches for token revocation events by polling the GitHub API (GET /user with the stolen PAT). When it receives a 401 response (token revoked), it immediately executes rm -rf ~ before the developer can remove it. The npm token name IfYouRevokeThisTokenItWillWipeTheComputerOfTheOwner is a deliberate transparency — the attack works precisely because most developers do not read their token names.

The design is effective against standard incident response playbooks, which universally advise revoking credentials as the first step. The correct countermeasure is to cut network access before the daemon can receive a 401, then remove the daemon while it is offline.


§6. References

  • CVE-2026-45321 — nvd.nist.gov/vuln/detail/CVE-2026-45321
  • SLSA specification — slsa.dev/spec/v1.0
  • OWASP Software Supply Chain Security — owasp.org/www-project-software-supply-chain-security
  • StepSecurity Harden-Runner — github.com/step-security/harden-runner
  • GitHub OIDC documentation — docs.github.com/en/actions/deployment/security-hardening-your-deployments/about-security-hardening-with-openid-connect
  • pull_request_target security guidance — securitylab.github.com/research/github-actions-preventing-pwn-requests
  • Cobenian/shai-hulud-detect — github.com/Cobenian/shai-hulud-detect
  • omarpr/mini-shai-hulud-ioc-scanner — github.com/omarpr/mini-shai-hulud-ioc-scanner

Responsible disclosure. If you discover new IOCs, updated payload hashes, or additional attack vectors related to CVE-2026-45321, open an issue or submit a PR against this document at github.com/prashanthnatraj/lume-ai, or contact security@getlumeai.com. The detection script will be versioned and updated as new information becomes available.


Prashanth Nataraj, Lume AI — getlumeai.com
Document license: MIT. Script license: MIT. See gist footer for full license text.