platform/mini-shai-hulud-scan/SKILL.md

name: mini-shai-hulud-scan description: >- Triage a macOS/Linux dev machine for compromise from the May 11, 2026 Mini Shai-Hulud npm + PyPI supply-chain attack (TanStack, Mistral AI, UiPath, OpenSearch, mistralai==2.4.6, guardrails-ai==0.10.1). Use when the request names this campaign or its IOCs — Shai-Hulud, TanStack compromise, mistralai 2.4.6, guardrails-ai 0.10.1, router_init.js, filev2.getsession.org — or an install ran on May 11–12, 2026. For a general "is my machine compromised" review not tied to this wave, use workstation-compromise-scan.

mini-shai-hulud-scan

Run an end-to-end IOC sweep for the Mini Shai-Hulud campaign (TanStack npm + Mistral AI / Guardrails PyPI compromise, malicious window 2026-05-11 19:20–19:26 UTC, 170+ npm packages and 2 PyPI packages affected).

When to use this

  • User asks "is my machine compromised?" in the context of npm/PyPI supply-chain attacks
  • User mentions any of: Shai-Hulud, TanStack compromise, mistralai==2.4.6, guardrails-ai==0.10.1, router_init.js, filev2.getsession.org, api.masscan.cloud, git-tanstack.com, IfYouRevokeThisTokenItWillWipeTheComputerOfTheOwner
  • User ran npm/pnpm/yarn/bun install or pip install mistralai/guardrails-ai on May 11–12, 2026

Browsing/email/normal app usage is not at risk. Only developer machines, CI runners, build servers, and containers that installed or imported compromised packages need to worry.

Workflow

Step 0 — Inventory workspaces

Find code roots so scans target real workspaces (not /, not Library/). Cache these paths and reuse them in every step.

# Code-bearing workspaces under $HOME (skip Library, Trash, hidden caches that don't have user code)
find "$HOME" -maxdepth 4 -name "node_modules" -type d \
  -not -path "*/Library/*" -not -path "*/.Trash/*" 2>/dev/null \
  | awk -F/node_modules '{print $1}' | sort -u

# Project package.json roots (no node_modules)
find "$HOME" -maxdepth 4 -name "package.json" \
  -not -path "*/node_modules/*" -not -path "*/Library/*" 2>/dev/null \
  | xargs -n1 dirname | sort -u

# Python venvs (used in step 6)
find "$HOME" -maxdepth 6 -type d -name "site-packages" \
  -not -path "*/Library/*" 2>/dev/null

Scan the roots the commands above discover (typically ~/dev, ~/src, ~/Documents/dev, ~/Sites, plus per-project dirs directly under ~); list them in the report, and ask only if discovery comes back empty or the user keeps checkouts outside $HOME.

Step 1 — Lockfile / manifest scan (informational only)

Search outside node_modules for the affected package families. Matches here are normal; they just identify projects that could be affected — step 2 is what determines actual compromise.

find <WORKSPACES> -type f \( \
  -name "package-lock.json" -o -name "pnpm-lock.yaml" -o -name "yarn.lock" -o \
  -name "bun.lock" -o -name "bun.lockb" -o -name "package.json" -o \
  -name "requirements.txt" -o -name "poetry.lock" -o -name "Pipfile.lock" \
\) -not -path "*/node_modules/*" -print0 2>/dev/null \
| xargs -0 grep -lE \
  '@tanstack|@mistralai|@uipath|@opensearch-project|@squawk|@tallyui|mistralai|guardrails-ai|79ac49eedf774dd4b0cfa308722bc463cfe5885c|@tanstack/setup' \
  2>/dev/null

Step 2 — Definitive payload / hash check (THIS is the IOC)

Look for the malicious payload files and the malicious commit hash inside installed @tanstack/* packages. Only these hits indicate compromise.

# Payload filenames in any installed node_modules
find <WORKSPACES> -path "*/node_modules/*" \( \
  -name "router_init.js" -o -name "router_runtime.js" \
\) -print 2>/dev/null

# Malicious commit hash / optionalDependencies marker inside installed @tanstack package.json files
find <WORKSPACES> -path "*/node_modules/@tanstack/*/package.json" -print0 2>/dev/null \
| xargs -0 grep -lE \
  '79ac49eedf774dd4b0cfa308722bc463cfe5885c|"router_init"|tanstack/router#' \
  2>/dev/null

Step 3 — IOC string sweep across code and global caches

grep -rE \
  '79ac49eedf774dd4b0cfa308722bc463cfe5885c|filev2\.getsession\.org|api\.masscan\.cloud|git-tanstack\.com|litter\.catbox\.moe|IfYouRevokeThisTokenItWillWipeTheComputerOfTheOwner' \
  <WORKSPACES> ~/.npm ~/.bun ~/.cache/opencode ~/.config/opencode 2>/dev/null

Step 4 — Project-level persistence artifacts

# Exact-path persistence files (high-confidence IOCs)
find "$HOME" -type f \( \
  -path "*/.claude/router_runtime.js" -o \
  -path "*/.claude/setup.mjs" -o \
  -path "*/.vscode/setup.mjs" -o \
  -path "*/.github/workflows/codeql_analysis.yml" \
\) -not -path "*/Library/*" -not -path "*/node_modules/*" -print 2>/dev/null

# tasks.json hits are NOISY (VS Code extensions, vendored libs). Collect them, then
# escalate ONLY if any contain malicious commands:
find "$HOME" -type f -path "*/.vscode/tasks.json" \
  -not -path "*/Library/*" -not -path "*/node_modules/*" -print0 2>/dev/null \
| xargs -0 grep -lE \
  'setup\.mjs|router_runtime|router_init|getsession|masscan|git-tanstack|catbox\.moe|curl |wget |base64 |child_process|eval\(' \
  2>/dev/null

If a tasks.json shows up in the noisy first pass but the second (content-grep) pass is empty, it's benign.

Step 5 — OS-level persistence

ls -la \
  ~/.local/bin/gh-token-monitor.sh \
  ~/.config/systemd/user/gh-token-monitor.service \
  ~/Library/LaunchAgents/com.user.gh-token-monitor.plist \
  2>&1

Also eyeball ~/Library/LaunchAgents/ for any unknown entries (filter out standard com.apple.*, com.google.*, com.adobe.*, com.microsoft.*, com.dropbox.*, com.docker.*, com.aws.*).

Step 6 — Python: every venv + system pip

# Per-venv check
find "$HOME" -maxdepth 6 -type d -name "site-packages" -not -path "*/Library/*" 2>/dev/null \
| while read sp; do
    for pkg in mistralai guardrails_ai; do
      ver=$(ls -d "$sp/${pkg}-"*.dist-info 2>/dev/null | sed 's/.*-//;s/\.dist-info//' | head -1)
      if [ -n "$ver" ]; then echo "FOUND: $sp/$pkg version=$ver"; fi
    done
  done

# Active env + system + user pip
python3 -m pip list 2>/dev/null | grep -iE "mistralai|guardrails"
pip3 list --user 2>/dev/null   | grep -iE "mistralai|guardrails"

Compromised versions: mistralai==2.4.6, guardrails-ai==0.10.1. Known-good baselines: mistralai<=2.4.5, guardrails-ai<=0.10.0.

Step 7 — PyPI payload artifact

ls -la /tmp/transformers.pyz 2>&1

Existence strongly indicates the PyPI payload ran.

Step 8 — Git activity audit (window: 2026-05-10 → 2026-05-13)

for d in <WORKSPACES>; do
  [ -d "$d/.git" ] || continue
  git -C "$d" log --all --since=2026-05-10 --until=2026-05-13 \
    --pretty=format:'%h %an %ae %s' 2>/dev/null \
  | grep -iE 'claude@users\.noreply\.github\.com|tanstack/setup|router_init|setup-formatter|dependabot/github_actions/format' \
    && echo "  ^^ HIT in $d"
  git -C "$d" branch -a 2>/dev/null \
    | grep -iE 'dependabot/github_actions/format|setup-formatter|shai-hulud' \
    && echo "  ^^ HIT in $d"
done

Also check for an injected workflow:

find <WORKSPACES> -path "*/.github/workflows/codeql_analysis.yml" -print 2>/dev/null

Interpreting results

Definite compromise = any hit in steps 2, 3, 4 (exact paths), 5, 6 (with the compromised version), 7, 8.

Benign / informational = lockfile family matches in step 1 with no step-2 hit; tasks.json hits in step 4 that survive only the first pass.

A clean report = all of these true:

- No router_init.js / router_runtime.js anywhere
- No 79ac49eed... commit hash in any @tanstack package.json
- No IOC strings anywhere
- No project persistence files (.claude/router_runtime.js, .claude/setup.mjs, .vscode/setup.mjs, malicious codeql_analysis.yml, or tasks.json with suspicious commands)
- No gh-token-monitor.* in ~/.local/bin, systemd user, or LaunchAgents
- No mistralai==2.4.6 or guardrails-ai==0.10.1 in any Python env
- No /tmp/transformers.pyz
- No suspicious commits/branches in the 2026-05-10..05-13 window

If anything hits — remediation order matters

These are the user's actions, in this order — present them as the plan with the evidence behind each. Delete, unload, or revoke only after evidence is captured and the user says go; a scan the user asked for is not authority to change the host.

  1. Stop using the machine for dev. Disconnect from VPN / cloud admin sessions. For CI: disable the runner, preserve logs, don't reconnect.
  2. Remove persistence FIRST (before rotating secrets — otherwise the malware re-exfiltrates the new ones):
    • systemctl --user stop/disable gh-token-monitor.service; delete ~/.config/systemd/user/gh-token-monitor.service, ~/.local/bin/gh-token-monitor.sh, ~/.config/gh-token-monitor
    • launchctl unload ~/Library/LaunchAgents/com.user.gh-token-monitor.plist; delete the plist
    • In each affected repo: git diff .claude/settings.json and .vscode/tasks.json for evidence, then remove .claude/router_runtime.js, .claude/setup.mjs, .vscode/setup.mjs, and the injected .github/workflows/codeql_analysis.yml
  3. Rotate secrets from a CLEAN host (never from the suspect one): npm tokens, GitHub PATs and fine-grained tokens, Actions/OIDC publishing grants, AWS/GCP/Azure keys, Vault tokens, K8s service-account tokens, SSH keys, DB credentials, anything in .env, AI-tool session logs/cookies.
  4. DO NOT revoke any npm token whose description contains IfYouRevokeThisTokenItWillWipeTheComputerOfTheOwner from the suspect host — the malware booby-traps revocation. Revoke from a clean machine.
  5. Audit GitHub / npm activity after 2026-05-11T19:20Z: publish logs, Actions runs, unexpected branches/PRs, outbound traffic to filev2.getsession.org, api.masscan.cloud, git-tanstack.com, litter.catbox.moe.
  6. Rebuild, don't just clean, CI runners and dev VMs. Purge dependency caches (npm, pnpm, bun, pip, poetry, Actions cache), rebuild base images, regenerate lockfiles against known-good versions, redeploy. TanStack's RCA shows the attack pivoted through GitHub Actions cache poisoning + OIDC token extraction — stale caches deserve special attention.

Prevention to recommend after a clean scan

Release-age delays would have blocked this attack (malicious versions lived for ~3 hours):

# ~/.npmrc
min-release-age=7
ignore-scripts=true
# .npmrc (pnpm)
minimum-release-age=10080
# bunfig.toml
[install]
minimumReleaseAge = 604800
# pyproject.toml (uv)
[tool.uv]
exclude-newer = "7 days"

Notes

  • IOCs and version data sourced from TanStack's postmortem, SafeDep, StepSecurity, and Snyk write-ups.
  • The malicious window is narrow (3–6 minutes for the npm packages, ~hours total for the broader campaign), so if no installs happened on 2026-05-11 the practical risk is near-zero — but still run the scan to be sure.
  • Run scans from the user's code roots, not / — full-FS scans are slow and produce noise from Library/ extension bundles.