| 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 |
Shai-Hulud IOC Scanner
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.
Rules
- NEVER run any command without showing it to the user first and getting approval via AskUserQuestion. This is the single most important rule of this skill.
- NEVER skip Pass 1 (dead-man's switch). It MUST run first. If the worm is present and the user revokes tokens before disabling the dead-man's switch, it triggers
rm -rf ~/.
- Platform detection (Step 0) is the ONLY exception to the approval rule - it runs
uname -s and checks /proc/version, which are harmless read-only introspection.
- This scan is READ-ONLY. Never delete, modify, or quarantine files. Remediation instructions are printed but never executed.
- If a command fails (permission denied, command not found), report the failure and move on. Never retry with sudo unless the user explicitly asks.
- On WSL, always check BOTH the Linux side AND the Windows side.
- Each pass is independent. If the user skips a pass, record it as SKIPPED and move to the next.
- Pass 5 (Network IOCs) is opt-in - only offered after passes 1-4 complete.
Step 0: Platform Detection
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.
Step 1: Introduction
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 (gh-token-monitor)
What to tell the user
### 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.
Commands by platform
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
Flow
- Print the explanation above
- Print the platform-appropriate commands in a fenced code block
- Use
AskUserQuestion with:
- question: "Pass 1 will run the commands shown above to check for the dead-man's switch. Approve?"
- options: "Approve - run these commands" / "Skip - move to Pass 2"
- If approved: run each command via Bash, collect output
- Determine result:
- If ANY command outputs "FOUND": result is INFECTED
- If all commands output "NOT FOUND": result is CLEAN
- Print result:
Pass 1 - Dead-man's switch: CLEAN or Pass 1 - Dead-man's switch: INFECTED - [details]
- If INFECTED: print immediate warning - "DO NOT revoke any tokens or credentials until this daemon is removed. See remediation steps at the end of the scan."
Pass 2: Persistence Hooks
What to tell the user
### 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.
Commands (same across all platforms)
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"
Flow
- Print explanation
- Print commands in a fenced code block
AskUserQuestion: "Pass 2 will run the commands shown above to check for persistence hooks. Approve?" - options: "Approve" / "Skip"
- If approved: run commands, collect output
- Result: INFECTED if any "FOUND" or if grep matches content (not the "CLEAN" fallback). CLEAN otherwise.
- Print result
Pass 3: Malicious Files on Disk
What to tell the user
### 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).
Commands
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:
- macOS:
shasum -a 256 <path>
- Linux/WSL:
sha256sum <path>
Known malicious hashes:
ab4fcadaec49c03278063dd269ea5eef82d24f2124a8e15d7b90f2fa8601266c
2ec78d556d696e208927cc503d48e4b5eb56b31abc2870c2ed2e98d6be27fc96
Windows (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
Flow
- Print explanation
- Print targeted scan commands in a fenced code block
AskUserQuestion: "Pass 3 will run the commands shown above to scan for malicious files. Approve?" - options: "Approve" / "Skip"
- If approved: run commands, collect output
- If any files found: hash them and compare against known malicious hashes
- Print result
- After the targeted scan completes, use
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"
- If yes, show the broader commands and ask approval again:
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 Package Versions
What to tell the user
### 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
Scope selection
Before showing commands, use AskUserQuestion:
- question: "Where should I scan for compromised packages?"
- options:
- "Current directory only" - scans
. with maxdepth 5
- "Specific path" - user provides a path (use "Other" option)
- "All projects under home directory" - scans
~ with maxdepth 6 (slower)
Commands (using selected scope as <scope>)
macOS/Linux/WSL:
find <scope> -maxdepth <depth> \( -name "package-lock.json" -o -name "pnpm-lock.yaml" -o -name "yarn.lock" \) -not -path "*/node_modules/*" 2>/dev/null
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
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
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
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
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
Flow
- Print explanation with affected versions list
- Ask user for scan scope via
AskUserQuestion
- Build commands using selected scope
- Print the exact commands in a fenced code block
AskUserQuestion: "Pass 4 will run the commands shown above to check for compromised packages. Approve?" - options: "Approve" / "Skip"
- If approved: run commands, collect output
- Print result
Pass 5: Network IOCs (Opt-in)
Offering the pass
After passes 1-4 complete, use AskUserQuestion:
- question: "Passes 1-4 are complete. Would you also like to check for network-level IOCs? This checks if your machine can resolve known C2 domains and if any process is actively connecting to attacker infrastructure."
- options: "Yes - show me the commands" / "No - skip to results"
If user declines, record as SKIPPED and go to summary.
What to tell the user (if opted in)
### 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.
Commands by platform
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
Flow
- Print explanation
- Print the platform-appropriate commands in a fenced code block
AskUserQuestion: "Pass 5 will run the commands shown above to check for network IOCs. Approve?" - options: "Approve" / "Skip"
- If approved: run commands, collect output
- Print result with context (DNS resolution alone is not proof of infection)
Summary
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.
Remediation
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."
IOC Reference
File hashes (SHA256)
ab4fcadaec49c03278063dd269ea5eef82d24f2124a8e15d7b90f2fa8601266c - router_init.js
2ec78d556d696e208927cc503d48e4b5eb56b31abc2870c2ed2e98d6be27fc96 - tanstack_runner.js
2258284d65f63829bd67eaba01ef6f1ada2f593f9bbe41678b2df360bd90d3df - setup.mjs
C2 infrastructure
git-tanstack.com (typosquat domain)
api.masscan.cloud (secrets exfiltration)
filev2.getsession.org (Session Protocol exfil)
83.142.209.194 (mistralai credential stealer)
Lockfile markers
voicproducoes (attacker npm account)
79ac49eedf (orphan commit hash prefix)
Persistence paths
~/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.yml
Campaign strings
IfYouRevokeThisTokenItWillWipeTheComputerOfTheOwner (commit message)
Shai-Hulud: Here We Go Again (repo description)
claude@users.noreply.github.com (dead-drop author)
svksjrhjkcejg (PBKDF2 salt)