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 installorpip install mistralai/guardrails-aion 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.
- Stop using the machine for dev. Disconnect from VPN / cloud admin sessions. For CI: disable the runner, preserve logs, don't reconnect.
- 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-monitorlaunchctl unload ~/Library/LaunchAgents/com.user.gh-token-monitor.plist; delete the plist- In each affected repo:
git diff.claude/settings.jsonand.vscode/tasks.jsonfor evidence, then remove.claude/router_runtime.js,.claude/setup.mjs,.vscode/setup.mjs, and the injected.github/workflows/codeql_analysis.yml
- 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. - DO NOT revoke any npm token whose description contains
IfYouRevokeThisTokenItWillWipeTheComputerOfTheOwnerfrom the suspect host — the malware booby-traps revocation. Revoke from a clean machine. - Audit GitHub / npm activity after
2026-05-11T19:20Z: publish logs, Actions runs, unexpected branches/PRs, outbound traffic tofilev2.getsession.org,api.masscan.cloud,git-tanstack.com,litter.catbox.moe. - 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 fromLibrary/extension bundles.
