一键导入
shai-hulud-scan
Scan machine for Mini Shai-Hulud (CVE-2026-45321) supply chain worm IOCs with full transparency - shows every command before execution
用 Codex 或 Claude 帮你安装 复制这段 Prompt,粘贴到 Codex、Claude 或其他助手里,让它检查 Skill 页面并帮你完成安装。
菜单
Scan machine for Mini Shai-Hulud (CVE-2026-45321) supply chain worm IOCs with full transparency - shows every command before execution
用 Codex 或 Claude 帮你安装 复制这段 Prompt,粘贴到 Codex、Claude 或其他助手里,让它检查 Skill 页面并帮你完成安装。
基于 SOC 职业分类
| name | shai-hulud-scan |
| description | Scan machine for Mini Shai-Hulud (CVE-2026-45321) supply chain worm IOCs with full transparency - shows every command before execution |
| allowed-tools | Bash, Read, AskUserQuestion, WebSearch, WebFetch |
| user-invocable | true |
Scans the current machine for indicators of compromise from the Mini Shai-Hulud supply chain worm (CVE-2026-45321, CVSS 9.6). This worm compromised 170+ npm/PyPI packages including @tanstack/, @mistralai/, @uipath/, and @opensearch-project/.
The scan is read-only and fully transparent - every command is shown to the user before execution, and nothing runs without explicit approval.
rm -rf ~/.uname -s and checks /proc/version, which are harmless read-only introspection.Run silently (no approval needed):
uname -s 2>/dev/null
If result is Darwin -> platform is macOS.
If result is Linux -> check for WSL:
grep -qi microsoft /proc/version 2>/dev/null && echo "WSL" || echo "LINUX"
If uname is unavailable, assume Windows (PowerShell environment).
Print to user: Detected platform: <platform>. All commands below are tailored for this environment.
Print this to the user before starting passes:
## Shai-Hulud IOC Scan (CVE-2026-45321)
This scan checks your machine for indicators of the Mini Shai-Hulud supply chain worm
in 4 passes (+ 1 optional). Each pass will:
1. Explain what it checks and why
2. Show you the exact commands it will run
3. Ask for your approval before running anything
Pass order matters - Pass 1 (dead-man's switch) runs first because the worm
destroys your home directory if you revoke credentials before disabling it.
Starting scan...
### Pass 1: Dead-Man's Switch
The worm installs a daemon called `gh-token-monitor` that polls GitHub every 60 seconds.
If it detects that your GitHub token has been revoked, it executes `rm -rf ~/` - wiping
your entire home directory. This is why we check for it FIRST, before anything else.
This pass checks for the daemon's service files and whether it's currently running.
macOS:
test -f ~/Library/LaunchAgents/com.user.gh-token-monitor.plist && echo "FOUND: LaunchAgent plist" || echo "NOT FOUND: LaunchAgent plist"
launchctl list 2>/dev/null | grep gh-token-monitor || echo "NOT FOUND: not loaded in launchctl"
test -f ~/.local/bin/gh-token-monitor.sh && echo "FOUND: gh-token-monitor.sh script" || echo "NOT FOUND: gh-token-monitor.sh script"
Linux:
test -f ~/.config/systemd/user/gh-token-monitor.service && echo "FOUND: systemd service" || echo "NOT FOUND: systemd service"
systemctl --user is-active gh-token-monitor 2>/dev/null && echo "FOUND: service is ACTIVE" || echo "NOT FOUND: service not active"
test -f ~/.local/bin/gh-token-monitor.sh && echo "FOUND: gh-token-monitor.sh script" || echo "NOT FOUND: gh-token-monitor.sh script"
WSL (run BOTH): Linux commands above, PLUS:
powershell.exe -Command "Get-ScheduledTask | Where-Object { \$_.TaskName -like '*gh-token-monitor*' } | Format-Table TaskName,State" 2>/dev/null || echo "Could not query Windows scheduled tasks"
Windows (PowerShell):
Get-ScheduledTask | Where-Object { $_.TaskName -like '*gh-token-monitor*' } | Format-Table TaskName,State
AskUserQuestion with:
Pass 1 - Dead-man's switch: CLEAN or Pass 1 - Dead-man's switch: INFECTED - [details]### Pass 2: Persistence Hooks
The worm injects itself into developer tool configurations so it re-executes whenever
you open your IDE or run Claude Code. It targets three places:
- Claude Code: injects hooks into settings.json, drops router_runtime.js and setup.mjs
- VS Code: adds malicious tasks to tasks.json, drops setup.mjs
- GitHub Actions: injects a fake codeql_analysis.yml workflow
This pass checks all three.
grep -i "router_runtime\|setup\.mjs" ~/.claude/settings.json 2>/dev/null || echo "CLEAN: ~/.claude/settings.json"
grep -i "router_runtime\|setup\.mjs" .claude/settings.json 2>/dev/null || echo "CLEAN: .claude/settings.json"
test -f ~/.claude/router_runtime.js && echo "FOUND: ~/.claude/router_runtime.js" || echo "NOT FOUND: ~/.claude/router_runtime.js"
test -f ~/.claude/setup.mjs && echo "FOUND: ~/.claude/setup.mjs" || echo "NOT FOUND: ~/.claude/setup.mjs"
grep -i "setup\.mjs\|router_runtime" .vscode/tasks.json 2>/dev/null || echo "CLEAN: .vscode/tasks.json"
test -f .vscode/setup.mjs && echo "FOUND: .vscode/setup.mjs" || echo "NOT FOUND: .vscode/setup.mjs"
test -f .github/workflows/codeql_analysis.yml && echo "FOUND: .github/workflows/codeql_analysis.yml - needs manual review" || echo "NOT FOUND: .github/workflows/codeql_analysis.yml"
AskUserQuestion: "Pass 2 will run the commands shown above to check for persistence hooks. Approve?" - options: "Approve" / "Skip"### Pass 3: Malicious Files
The worm drops specific files that may persist even after removing the compromised
package. The main payload is `router_init.js` (2.3MB obfuscated credential stealer).
Other artifacts include `tanstack_runner.js` and `setup.mjs` in unexpected locations.
This pass does a targeted scan of known directories first (fast).
macOS/Linux/WSL - targeted scan:
find . ~/.claude ~/.local ~/.config .vscode -maxdepth 3 -name "router_init.js" 2>/dev/null
find . ~/.claude ~/.local ~/.config .vscode -maxdepth 3 -name "tanstack_runner.js" 2>/dev/null
find . ~/.claude ~/.local ~/.config .vscode -maxdepth 3 -name "setup.mjs" 2>/dev/null
If router_init.js is found, hash it:
shasum -a 256 <path>sha256sum <path>Known malicious hashes:
ab4fcadaec49c03278063dd269ea5eef82d24f2124a8e15d7b90f2fa8601266c2ec78d556d696e208927cc503d48e4b5eb56b31abc2870c2ed2e98d6be27fc96Windows (PowerShell):
Get-ChildItem -Path ., $env:USERPROFILE\.claude, $env:USERPROFILE\.local, $env:USERPROFILE\.config, .\.vscode -Recurse -Depth 3 -Filter "router_init.js" -ErrorAction SilentlyContinue
Get-ChildItem -Path ., $env:USERPROFILE\.claude, $env:USERPROFILE\.local, $env:USERPROFILE\.config, .\.vscode -Recurse -Depth 3 -Filter "tanstack_runner.js" -ErrorAction SilentlyContinue
Get-ChildItem -Path ., $env:USERPROFILE\.claude, $env:USERPROFILE\.local, $env:USERPROFILE\.config, .\.vscode -Recurse -Depth 3 -Filter "setup.mjs" -ErrorAction SilentlyContinue
If found, hash: Get-FileHash <path> -Algorithm SHA256
AskUserQuestion: "Pass 3 will run the commands shown above to scan for malicious files. Approve?" - options: "Approve" / "Skip"AskUserQuestion: "Targeted scan complete. Would you also like to scan your entire home directory? This is slower but more thorough." - options: "Yes - scan home directory" / "No - move to Pass 4"macOS/Linux/WSL - broad scan:
find ~ -maxdepth 5 -name "router_init.js" 2>/dev/null
find ~ -maxdepth 5 -name "tanstack_runner.js" 2>/dev/null
Show these commands, ask approval, run only if approved.
### Pass 4: Compromised Packages
The worm was distributed through specific versions of popular npm and PyPI packages.
This pass checks your lockfiles and node_modules for those exact versions.
Affected versions:
- @tanstack/*: 1.169.5, 1.169.8, 1.167.68, 1.167.71, 1.167.38, 1.167.41
- @mistralai/*: 2.2.2, 2.2.3, 2.2.4
- @opensearch-project/*: 3.5.3, 3.6.2, 3.7.0, 3.8.0
- @uipath/* (multiple versions)
- guardrails-ai (PyPI): 0.10.1
- mistralai (PyPI): 2.4.6
Before showing commands, use AskUserQuestion:
. with maxdepth 5~ with maxdepth 6 (slower)<scope>)macOS/Linux/WSL:
# Find lockfiles
find <scope> -maxdepth <depth> \( -name "package-lock.json" -o -name "pnpm-lock.yaml" -o -name "yarn.lock" \) -not -path "*/node_modules/*" 2>/dev/null
# Check lockfiles for IOC markers
find <scope> -maxdepth <depth> \( -name "package-lock.json" -o -name "pnpm-lock.yaml" -o -name "yarn.lock" \) -not -path "*/node_modules/*" -exec grep -l "voicproducoes\|79ac49eedf" {} \; 2>/dev/null
# Check for affected @tanstack/* versions in lockfiles
find <scope> -maxdepth <depth> \( -name "package-lock.json" -o -name "pnpm-lock.yaml" -o -name "yarn.lock" \) -not -path "*/node_modules/*" -exec grep -l "1\.169\.5\|1\.169\.8\|1\.167\.68\|1\.167\.71\|1\.167\.38\|1\.167\.41" {} \; 2>/dev/null
# Check for affected @mistralai/* versions
find <scope> -maxdepth <depth> \( -name "package-lock.json" -o -name "pnpm-lock.yaml" -o -name "yarn.lock" \) -not -path "*/node_modules/*" -exec grep -l "@mistralai.*2\.2\.2\|@mistralai.*2\.2\.3\|@mistralai.*2\.2\.4" {} \; 2>/dev/null
# Check for affected @opensearch-project/* versions
find <scope> -maxdepth <depth> \( -name "package-lock.json" -o -name "pnpm-lock.yaml" -o -name "yarn.lock" \) -not -path "*/node_modules/*" -exec grep -l "@opensearch-project.*3\.5\.3\|@opensearch-project.*3\.6\.2\|@opensearch-project.*3\.7\.0\|@opensearch-project.*3\.8\.0" {} \; 2>/dev/null
# Check for any @uipath/* packages
find <scope> -maxdepth <depth> \( -name "package-lock.json" -o -name "pnpm-lock.yaml" -o -name "yarn.lock" \) -not -path "*/node_modules/*" -exec grep -l "@uipath/" {} \; 2>/dev/null
Windows (PowerShell):
Get-ChildItem -Path <scope> -Recurse -Depth <depth> -Include "package-lock.json","pnpm-lock.yaml","yarn.lock" -ErrorAction SilentlyContinue | Select-String -Pattern "voicproducoes|79ac49eedf" | Select-Object -ExpandProperty Path -Unique
Get-ChildItem -Path <scope> -Recurse -Depth <depth> -Include "package-lock.json","pnpm-lock.yaml","yarn.lock" -ErrorAction SilentlyContinue | Select-String -Pattern "1\.169\.5|1\.169\.8|1\.167\.68|1\.167\.71" | Select-Object -ExpandProperty Path -Unique
AskUserQuestionAskUserQuestion: "Pass 4 will run the commands shown above to check for compromised packages. Approve?" - options: "Approve" / "Skip"After passes 1-4 complete, use AskUserQuestion:
If user declines, record as SKIPPED and go to summary.
### Pass 5: Network IOCs
This checks three things:
1. Whether the worm's C2 domains resolve from your machine (DNS check)
2. Whether your hosts file has suspicious entries
3. Whether any process is actively connecting to known C2 infrastructure
Note: DNS resolution alone does not mean you're infected - it just means the domains
are still active and your DNS isn't blocking them.
macOS:
dig +short git-tanstack.com 2>/dev/null || nslookup git-tanstack.com 2>/dev/null
dig +short api.masscan.cloud 2>/dev/null || nslookup api.masscan.cloud 2>/dev/null
grep -i "tanstack\|masscan\|voicproducoes\|getsession" /etc/hosts 2>/dev/null || echo "CLEAN: /etc/hosts"
lsof -i -nP 2>/dev/null | grep -i "83.142.209.194\|git-tanstack\|masscan" || echo "No active connections to known C2"
Linux:
dig +short git-tanstack.com 2>/dev/null || nslookup git-tanstack.com 2>/dev/null
dig +short api.masscan.cloud 2>/dev/null || nslookup api.masscan.cloud 2>/dev/null
grep -i "tanstack\|masscan\|voicproducoes\|getsession" /etc/hosts 2>/dev/null || echo "CLEAN: /etc/hosts"
ss -tnp 2>/dev/null | grep -i "83.142.209.194\|git-tanstack\|masscan" || echo "No active connections to known C2"
WSL (Linux commands above PLUS):
powershell.exe -Command "Get-Content C:\Windows\System32\drivers\etc\hosts | Select-String 'tanstack|masscan|voicproducoes'" 2>/dev/null || echo "CLEAN: Windows hosts file"
powershell.exe -Command "Get-NetTCPConnection -ErrorAction SilentlyContinue | Where-Object { \$_.RemoteAddress -eq '83.142.209.194' } | Format-Table" 2>/dev/null || echo "No active Windows connections to known C2"
Windows (PowerShell):
Resolve-DnsName git-tanstack.com -ErrorAction SilentlyContinue
Resolve-DnsName api.masscan.cloud -ErrorAction SilentlyContinue
Get-Content C:\Windows\System32\drivers\etc\hosts | Select-String 'tanstack|masscan|voicproducoes'
Get-NetTCPConnection | Where-Object { $_.RemoteAddress -eq '83.142.209.194' } | Format-Table
AskUserQuestion: "Pass 5 will run the commands shown above to check for network IOCs. Approve?" - options: "Approve" / "Skip"After all passes, print the results table:
## Scan Complete - Results
| Pass | Check | Result |
|------|-----------------------|---------|
| 1 | Dead-man's switch | <result> |
| 2 | Persistence hooks | <result> |
| 3 | Malicious files | <result> |
| 4 | Compromised packages | <result> |
| 5 | Network IOCs | <result> |
CVE: CVE-2026-45321 | CVSS: 9.6 | Threat Actor: TeamPCP
Each <result> is one of: CLEAN, INFECTED, or SKIPPED.
Only print this section if ANY pass returned INFECTED.
## REMEDIATION - Follow this order exactly
The order below is critical. The dead-man's switch will destroy your home directory
if you revoke tokens before disabling it.
### Step 1: Disable the dead-man's switch FIRST
macOS:
launchctl unload ~/Library/LaunchAgents/com.user.gh-token-monitor.plist
rm ~/Library/LaunchAgents/com.user.gh-token-monitor.plist
rm ~/.local/bin/gh-token-monitor.sh
Linux/WSL:
systemctl --user stop gh-token-monitor.service
systemctl --user disable gh-token-monitor.service
rm ~/.config/systemd/user/gh-token-monitor.service
rm ~/.local/bin/gh-token-monitor.sh
Windows:
Unregister-ScheduledTask -TaskName "gh-token-monitor" -Confirm:$false
DO NOT revoke any tokens until this step is confirmed complete.
### Step 2: Remove persistence hooks
- Delete ~/.claude/router_runtime.js and ~/.claude/setup.mjs
- Remove malicious entries from ~/.claude/settings.json and .claude/settings.json
- Delete .vscode/setup.mjs and remove malicious tasks from .vscode/tasks.json
- Delete .github/workflows/codeql_analysis.yml if it was injected
### Step 3: Remove malicious files
- Delete any router_init.js and tanstack_runner.js files found in Pass 3
### Step 4: Fix compromised packages
- Delete node_modules and lockfiles in affected projects
- Clear package manager cache: npm cache clean --force / pnpm store prune
- Reinstall dependencies: npm install / pnpm install
### Step 5: Rotate ALL credentials (in this order)
1. npm publish tokens and OIDC federation grants
2. GitHub PATs and fine-grained tokens
3. AWS credentials (static keys and IMDSv2 roles)
4. HashiCorp Vault tokens
5. Kubernetes service account tokens
6. SSH private keys
7. GCP service account credentials
8. Any secrets visible in Claude Code session logs (~/.claude/projects/*.jsonl)
### Step 6: Block C2 infrastructure
Add to your DNS blocklist or /etc/hosts:
- git-tanstack.com
- api.masscan.cloud
- *.getsession.org (exfiltration network)
### Step 7: Verify
Run /shai-hulud-scan again to confirm all passes are CLEAN.
If Pass 1 was CLEAN but other passes are INFECTED, still warn: "Pass 1 was clean, but double-check the dead-man's switch is truly absent before rotating credentials."
ab4fcadaec49c03278063dd269ea5eef82d24f2124a8e15d7b90f2fa8601266c - router_init.js2ec78d556d696e208927cc503d48e4b5eb56b31abc2870c2ed2e98d6be27fc96 - tanstack_runner.js2258284d65f63829bd67eaba01ef6f1ada2f593f9bbe41678b2df360bd90d3df - setup.mjsgit-tanstack.com (typosquat domain)api.masscan.cloud (secrets exfiltration)filev2.getsession.org (Session Protocol exfil)83.142.209.194 (mistralai credential stealer)voicproducoes (attacker npm account)79ac49eedf (orphan commit hash prefix)~/Library/LaunchAgents/com.user.gh-token-monitor.plist (macOS)~/.config/systemd/user/gh-token-monitor.service (Linux)~/.local/bin/gh-token-monitor.sh (macOS/Linux)~/.claude/router_runtime.js~/.claude/setup.mjs.vscode/setup.mjs.github/workflows/codeql_analysis.ymlIfYouRevokeThisTokenItWillWipeTheComputerOfTheOwner (commit message)Shai-Hulud: Here We Go Again (repo description)claude@users.noreply.github.com (dead-drop author)svksjrhjkcejg (PBKDF2 salt)