Last active
May 15, 2026 09:31
-
-
Save gabrielstuff/1ec8cb4a351745895c76dfadc789090a to your computer and use it in GitHub Desktop.
A simple check for tanstack hack
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| #!/usr/bin/env bash | |
| # Mini Shai-Hulud / TanStack npm supply-chain compromise check. | |
| # Safe to run — read-only, no network calls, no modifications. | |
| # Works in bash and zsh. macOS + Linux. | |
| # | |
| # Based on IOCs published by Socket.dev (2026-05-11) and OpenAI's response | |
| # (2026-05-13). Covers TanStack, Mistral AI, OpenSearch, Guardrails AI, | |
| # Squawk, and the gh-token-monitor stealer drop. | |
| set +e | |
| set -u | |
| # ---------- colors ---------- | |
| RED=$'\033[31m'; GRN=$'\033[32m'; YLW=$'\033[33m'; BLD=$'\033[1m'; DIM=$'\033[2m'; RST=$'\033[0m' | |
| [ -t 1 ] || { RED=""; GRN=""; YLW=""; BLD=""; DIM=""; RST=""; } | |
| HITS=0 | |
| FINDINGS=() | |
| section() { printf '\n%s── %s ──%s\n' "$BLD" "$1" "$RST"; } | |
| ok() { printf ' %sok%s %s\n' "$GRN" "$RST" "$1"; } | |
| bad() { HITS=$((HITS + 1)); FINDINGS+=("$1"); printf ' %sHIT%s %s\n' "$RED" "$RST" "$1"; } | |
| note() { printf ' %s%s%s\n' "$DIM" "$1" "$RST"; } | |
| # Known malicious SHA-256s (Socket.dev + Mistral AI advisory) | |
| SHA_ROUTER_INIT="ab4fcadaec49c03278063dd269ea5eef82d24f2124a8e15d7b90f2fa8601266c" | |
| SHA_TANSTACK_RUNNER="2ec78d556d696e208927cc503d48e4b5eb56b31abc2870c2ed2e98d6be27fc96" | |
| SHA_TANSTACK_SETUP_PKG="7c12d8614c624c70d6dd6fc2ee289332474abaa38f70ebe2cdef064923ca3a9b" | |
| # Compromised commits in TanStack/router fork network | |
| BAD_COMMIT="79ac49eedf774dd4b0cfa308722bc463cfe5885c" | |
| BAD_COMMIT_FORK="65bf499d16a5e8d25ba95d69ec9790a6dd4a1f14" | |
| # Known C2 / attacker infra (domains + IP) | |
| BAD_HOSTS='filev2\.getsession\.org|seed[123]\.getsession\.org|git-tanstack\.com|api\.masscan\.cloud|litter\.catbox\.moe|83\.142\.209\.194' | |
| # Attacker GitHub identities | |
| BAD_GH_USERS='zblgg|voicproducoes|zblgg/configuration' | |
| # Compromised npm/PyPI artifacts (specific versions only) | |
| # Format: name@version, one per line | |
| BAD_PKGS=$(cat <<'EOF' | |
| @mistralai/mistralai@2.2.2 | |
| @mistralai/mistralai@2.2.3 | |
| @mistralai/mistralai@2.2.4 | |
| @mistralai/mistralai-azure@1.7.1 | |
| @mistralai/mistralai-azure@1.7.2 | |
| @mistralai/mistralai-azure@1.7.3 | |
| @mistralai/mistralai-gcp@1.7.1 | |
| @mistralai/mistralai-gcp@1.7.2 | |
| @mistralai/mistralai-gcp@1.7.3 | |
| @opensearch-project/opensearch@3.5.3 | |
| @opensearch-project/opensearch@3.6.2 | |
| @opensearch-project/opensearch@3.7.0 | |
| @opensearch-project/opensearch@3.8.0 | |
| @squawk/mcp@0.9.2 | |
| @squawk/mcp@0.9.3 | |
| @squawk/mcp@0.9.4 | |
| @squawk/mcp@0.9.5 | |
| @squawk/weather@0.5.10 | |
| @squawk/flightplan@0.5.6 | |
| guardrails-ai@0.10.1 | |
| mistralai@2.4.6 | |
| cross-stitch@1.1.4 | |
| cross-stitch@1.1.5 | |
| cross-stitch@1.1.6 | |
| cross-stitch@1.1.7 | |
| ts-dna@3.0.3 | |
| ts-dna@3.0.4 | |
| ts-dna@3.0.5 | |
| git-git-git@1.0.10 | |
| git-git-git@1.0.11 | |
| git-git-git@1.0.12 | |
| git-branch-selector@1.3.6 | |
| git-branch-selector@1.3.7 | |
| nextmove-mcp@0.1.5 | |
| nextmove-mcp@0.1.6 | |
| nextmove-mcp@0.1.7 | |
| cmux-agent-mcp@0.1.7 | |
| cmux-agent-mcp@0.1.8 | |
| EOF | |
| ) | |
| check_path() { | |
| local label="$1" p="$2" | |
| if [ -e "$p" ] || [ -L "$p" ]; then | |
| bad "$label exists → $p" | |
| ls -la "$p" 2>/dev/null | sed 's/^/ /' | |
| else | |
| ok "$label not present" | |
| fi | |
| } | |
| # pick a sha256 tool | |
| SHA="" | |
| command -v shasum >/dev/null 2>&1 && SHA="shasum -a 256" | |
| [ -z "$SHA" ] && command -v sha256sum >/dev/null 2>&1 && SHA="sha256sum" | |
| # ---------- header ---------- | |
| cat <<EOF | |
| ${BLD}Mini Shai-Hulud / TanStack supply-chain compromise check${RST} | |
| ${DIM}Scans for IOCs from the npm attack publicly disclosed 2026-05-11. | |
| Read-only. No network calls. Will NOT flag legitimate apps.${RST} | |
| host: $(hostname 2>/dev/null) | |
| user: $(id -un 2>/dev/null) | |
| os: $(uname -srm 2>/dev/null) | |
| shell: ${SHELL:-unknown} | |
| date: $(date -u '+%Y-%m-%dT%H:%M:%SZ') | |
| EOF | |
| # ---------- 1. malicious payload files anywhere (incl. node_modules) ---------- | |
| section "1. Payload files (router_init.js, tanstack_runner.js, vite_setup.mjs)" | |
| note "IOC files dropped into compromised TanStack/Mistral/etc. packages" | |
| note "scanning node_modules — that's where the files live" | |
| RI_LIST=$(find "$HOME" \ | |
| \( -name .Trash -o -name Library \) -prune \ | |
| -o -type f \( -name router_init.js -o -name tanstack_runner.js -o -name vite_setup.mjs \) -print 2>/dev/null) | |
| if [ -n "$RI_LIST" ]; then | |
| bad "malicious payload file(s) found" | |
| while IFS= read -r f; do | |
| printf ' %s\n' "$f" | |
| if [ -n "$SHA" ]; then | |
| h=$($SHA "$f" 2>/dev/null | awk '{print $1}') | |
| case "$h" in | |
| "$SHA_ROUTER_INIT"|"$SHA_TANSTACK_RUNNER") | |
| printf ' %s↳ SHA-256 MATCHES KNOWN MALWARE: %s%s\n' "$RED" "$h" "$RST" ;; | |
| *) | |
| printf ' %s↳ sha256: %s%s\n' "$DIM" "$h" "$RST" ;; | |
| esac | |
| fi | |
| done <<< "$RI_LIST" | |
| else | |
| ok "no router_init.js / tanstack_runner.js / vite_setup.mjs found" | |
| fi | |
| # ---------- 2. gh-token-monitor stealer persistence ---------- | |
| section "2. gh-token-monitor stealer persistence" | |
| check_path "launch script " "$HOME/.local/bin/gh-token-monitor.sh" | |
| check_path "systemd unit " "$HOME/.config/systemd/user/gh-token-monitor.service" | |
| check_path "macOS LaunchAgent" "$HOME/Library/LaunchAgents/com.user.gh-token-monitor.plist" | |
| # ---------- 3. agent / editor config dir drops ---------- | |
| section "3. Suspicious scripts in agent + editor config dirs" | |
| note "covers .claude .codex .opencode .cursor .windsurf .aider .continue .zed .trae .vscode" | |
| AGENT_DIRS='.claude|.codex|.opencode|.cursor|.windsurf|.aider|.continue|.zed|.trae|.vscode|.copilot|.github-copilot' | |
| DROP_HITS=$(find "$HOME" \ | |
| \( -name node_modules -o -name extensions -o -name .Trash -o -name Library \) -prune \ | |
| -o -type f \( -name 'setup.mjs' -o -name 'router_runtime.js' -o -name 'router_init.js' -o -name 'tanstack_runner.js' \) \ | |
| -print 2>/dev/null \ | |
| | grep -E "/($AGENT_DIRS)/") | |
| if [ -n "$DROP_HITS" ]; then | |
| bad "suspicious script(s) in agent/editor config dirs" | |
| printf '%s\n' "$DROP_HITS" | sed 's/^/ /' | |
| else | |
| ok "no suspicious scripts in agent/editor config dirs" | |
| fi | |
| # ---------- 4. .claude/settings.json hooks ---------- | |
| section "4. .claude/settings.json with unexpected hooks" | |
| SETTINGS_HITS="" | |
| while IFS= read -r f; do | |
| case "$f" in */node_modules/*) continue ;; esac | |
| if grep -Ei "router_init|router_runtime|tanstack_runner|vite_setup|setup\.mjs|$BAD_HOSTS" "$f" >/dev/null 2>&1; then | |
| SETTINGS_HITS="${SETTINGS_HITS}${f}"$'\n' | |
| fi | |
| done < <(find "$HOME" \ | |
| \( -name node_modules -o -name .Trash -o -name Library \) -prune \ | |
| -o -path '*/.claude/settings.json' -type f -print 2>/dev/null) | |
| if [ -n "$SETTINGS_HITS" ]; then | |
| bad ".claude/settings.json contains IOC strings" | |
| printf '%s' "$SETTINGS_HITS" | sed 's/^/ /' | |
| else | |
| ok ".claude/settings.json files clean" | |
| fi | |
| # ---------- 5. .vscode/tasks.json content ---------- | |
| section "5. .vscode/tasks.json containing payload" | |
| note "tasks.json is normal — only flagged if it contains IOC strings" | |
| TJ_HITS="" | |
| while IFS= read -r f; do | |
| case "$f" in */node_modules/*|*/extensions/*) continue ;; esac | |
| if grep -lEi "gh-token-monitor|router_init|router_runtime|tanstack_runner|vite_setup|setup\.mjs|$BAD_HOSTS|curl .*\| *(sh|bash|node)|wget .*\| *(sh|bash|node)" "$f" >/dev/null 2>&1; then | |
| TJ_HITS="${TJ_HITS}${f}"$'\n' | |
| fi | |
| done < <(find "$HOME" \ | |
| \( -name node_modules -o -name extensions -o -name .Trash -o -name Library \) -prune \ | |
| -o -path '*/.vscode/tasks.json' -type f -print 2>/dev/null) | |
| if [ -n "$TJ_HITS" ]; then | |
| bad "tasks.json with malware payload" | |
| printf '%s' "$TJ_HITS" | sed 's/^/ /' | |
| else | |
| ok "no tasks.json contains payload strings" | |
| fi | |
| # ---------- 6. /tmp/transformers.pyz (guardrails-ai variant) ---------- | |
| section "6. /tmp/transformers.pyz (guardrails-ai payload)" | |
| if [ -e /tmp/transformers.pyz ]; then | |
| bad "/tmp/transformers.pyz exists" | |
| ls -la /tmp/transformers.pyz 2>/dev/null | sed 's/^/ /' | |
| else | |
| ok "/tmp/transformers.pyz not present" | |
| fi | |
| # ---------- 7. compromised packages in lockfiles ---------- | |
| section "7. Compromised package versions in lockfiles" | |
| note "scans package-lock.json / pnpm-lock.yaml / yarn.lock / package.json / requirements.txt" | |
| LOCK_HITS="" | |
| LOCK_FILES=$(find "$HOME" \ | |
| \( -name node_modules -o -name .Trash -o -name Library -o -name .git \) -prune \ | |
| -o -type f \( -name 'package-lock.json' -o -name 'pnpm-lock.yaml' -o -name 'yarn.lock' -o -name 'package.json' -o -name 'requirements.txt' -o -name 'poetry.lock' -o -name 'uv.lock' \) -print 2>/dev/null) | |
| while IFS= read -r pv; do | |
| [ -z "$pv" ] && continue | |
| name="${pv%@*}" | |
| ver="${pv##*@}" | |
| # Build a tolerant pattern. e.g. "@tanstack/react-router" at "1.2.3" | |
| # Match common lockfile encodings. | |
| while IFS= read -r lf; do | |
| [ -z "$lf" ] && continue | |
| if grep -F -e "\"${name}\": \"${ver}\"" \ | |
| -e "\"${name}@${ver}\"" \ | |
| -e "${name}@${ver}:" \ | |
| -e "${name}==${ver}" \ | |
| -e "name = \"${name}\"" "$lf" >/dev/null 2>&1; then | |
| # second pass: if matched name= alone (poetry/uv), verify version proximity | |
| if grep -F -e "\"${name}\": \"${ver}\"" -e "\"${name}@${ver}\"" -e "${name}@${ver}:" -e "${name}==${ver}" "$lf" >/dev/null 2>&1 \ | |
| || awk -v n="$name" -v v="$ver" ' | |
| /^\[\[package\]\]/ {pkg=""; ver=""} | |
| $1=="name" {gsub(/"/,"",$3); pkg=$3} | |
| $1=="version" {gsub(/"/,"",$3); ver=$3} | |
| pkg==n && ver==v {found=1; exit} | |
| END{exit !found} | |
| ' "$lf" 2>/dev/null; then | |
| LOCK_HITS="${LOCK_HITS}${pv} ← ${lf}"$'\n' | |
| fi | |
| fi | |
| done <<< "$LOCK_FILES" | |
| done <<< "$BAD_PKGS" | |
| if [ -n "$LOCK_HITS" ]; then | |
| bad "compromised package version(s) installed/locked" | |
| printf '%s' "$LOCK_HITS" | sed 's/^/ /' | |
| else | |
| ok "no compromised package versions found in lockfiles" | |
| fi | |
| # ---------- 8. malicious optionalDependencies / commit hash ---------- | |
| section "8. package.json with attacker optionalDependencies or commit hash" | |
| OPT_HITS="" | |
| while IFS= read -r pj; do | |
| if grep -F -e "$BAD_COMMIT" -e "$BAD_COMMIT_FORK" -e '@tanstack/setup' -e 'github:tanstack/router#79ac49ee' -e 'zblgg/configuration' "$pj" >/dev/null 2>&1; then | |
| OPT_HITS="${OPT_HITS}${pj}"$'\n' | |
| fi | |
| done < <(find "$HOME" \ | |
| \( -name .Trash -o -name Library \) -prune \ | |
| -o -type f -name 'package.json' -print 2>/dev/null) | |
| if [ -n "$OPT_HITS" ]; then | |
| bad "package.json references attacker commit / @tanstack/setup" | |
| printf '%s' "$OPT_HITS" | sed 's/^/ /' | |
| else | |
| ok "no package.json references attacker commit" | |
| fi | |
| # ---------- 9. running processes ---------- | |
| section "9. Running processes" | |
| PROC_HITS=$(ps -Aewwo pid,user,command 2>/dev/null \ | |
| | grep -Ei 'gh-token-monitor|router_init|router_runtime|tanstack_runner|vite_setup|\.claude/setup\.mjs|\.vscode/setup\.mjs|transformers\.pyz|MISTRAL_INIT=1' \ | |
| | grep -v grep) | |
| if [ -n "$PROC_HITS" ]; then | |
| bad "suspicious process(es) running" | |
| printf '%s\n' "$PROC_HITS" | sed 's/^/ /' | |
| else | |
| ok "no suspicious processes running" | |
| fi | |
| # ---------- 10. LaunchAgents/Daemons (macOS) ---------- | |
| if [ "$(uname)" = "Darwin" ]; then | |
| section "10. macOS LaunchAgents/Daemons referencing malware" | |
| LA_HITS=$(find \ | |
| "$HOME/Library/LaunchAgents" \ | |
| "/Library/LaunchAgents" \ | |
| "/Library/LaunchDaemons" \ | |
| -type f -name '*.plist' 2>/dev/null \ | |
| | xargs grep -lEi "gh-token-monitor|router_init|router_runtime|tanstack_runner|vite_setup|$BAD_HOSTS" 2>/dev/null) | |
| if [ -n "$LA_HITS" ]; then | |
| bad "LaunchAgent/Daemon references malware" | |
| printf '%s\n' "$LA_HITS" | sed 's/^/ /' | |
| else | |
| ok "no LaunchAgent/Daemon references malware" | |
| fi | |
| fi | |
| # ---------- 11. shell rc + cron ---------- | |
| section "11. Shell startup files + crontab" | |
| RC_FILES=("$HOME/.bashrc" "$HOME/.bash_profile" "$HOME/.zshrc" "$HOME/.zprofile" "$HOME/.profile") | |
| RC_HITS="" | |
| for rc in "${RC_FILES[@]}"; do | |
| [ -f "$rc" ] || continue | |
| if grep -Ei "gh-token-monitor|router_init|router_runtime|tanstack_runner|vite_setup|setup\.mjs|$BAD_HOSTS|MISTRAL_INIT" "$rc" >/dev/null 2>&1; then | |
| RC_HITS="${RC_HITS}${rc}"$'\n' | |
| fi | |
| done | |
| if [ -n "$RC_HITS" ]; then | |
| bad "shell startup file modified" | |
| printf '%s' "$RC_HITS" | sed 's/^/ /' | |
| else | |
| ok "shell startup files clean" | |
| fi | |
| CRON_OUT=$(crontab -l 2>/dev/null | grep -Ei "gh-token-monitor|router_init|router_runtime|vite_setup|setup\.mjs|transformers\.pyz|$BAD_HOSTS") | |
| if [ -n "$CRON_OUT" ]; then | |
| bad "crontab contains malware reference" | |
| printf '%s\n' "$CRON_OUT" | sed 's/^/ /' | |
| else | |
| ok "crontab clean" | |
| fi | |
| # ---------- 12. C2 hosts in /etc/hosts ---------- | |
| section "12. C2 domains in /etc/hosts" | |
| HOSTS_HITS=$(grep -Ei "$BAD_HOSTS" /etc/hosts 2>/dev/null) | |
| if [ -n "$HOSTS_HITS" ]; then | |
| bad "/etc/hosts mentions C2 domain" | |
| printf '%s\n' "$HOSTS_HITS" | sed 's/^/ /' | |
| else | |
| ok "/etc/hosts does not mention known C2 domains" | |
| fi | |
| # ---------- 13. git commits attributed to claude@users.noreply ---------- | |
| # Only spot-check repos under common dev dirs; full-disk scan would be too slow. | |
| section "13. Recent git commits as claude@users.noreply.github.com" | |
| note "looks across ~/Developer ~/Projects ~/Code ~/src ~/repos for repos with such commits" | |
| note "Socket.dev: attacker forged commits under this identity" | |
| GIT_HITS="" | |
| for base in "$HOME/Developer" "$HOME/Projects" "$HOME/Code" "$HOME/src" "$HOME/repos" "$HOME/work"; do | |
| [ -d "$base" ] || continue | |
| while IFS= read -r gd; do | |
| repo="${gd%/.git}" | |
| out=$(git -C "$repo" log --all --since='2026-04-01' --author='claude@users.noreply.github.com' --pretty='%h %ad %s' --date=short 2>/dev/null | head -n 5) | |
| if [ -n "$out" ]; then | |
| GIT_HITS="${GIT_HITS}${repo}:"$'\n'"${out}"$'\n\n' | |
| fi | |
| done < <(find "$base" -maxdepth 4 -type d -name .git 2>/dev/null) | |
| done | |
| if [ -n "$GIT_HITS" ]; then | |
| bad "commits authored as claude@users.noreply.github.com (verify each was made by the legit Claude Code App)" | |
| printf '%s' "$GIT_HITS" | sed 's/^/ /' | |
| else | |
| ok "no recent commits by claude@users.noreply.github.com in scanned repos" | |
| fi | |
| # ---------- 14. git remotes pointing at attacker fork ---------- | |
| section "14. Git remotes referencing attacker fork or commits" | |
| REMOTE_HITS="" | |
| for base in "$HOME/Developer" "$HOME/Projects" "$HOME/Code" "$HOME/src" "$HOME/repos" "$HOME/work"; do | |
| [ -d "$base" ] || continue | |
| while IFS= read -r gd; do | |
| repo="${gd%/.git}" | |
| rem=$(git -C "$repo" remote -v 2>/dev/null | grep -Ei "$BAD_GH_USERS") | |
| if [ -n "$rem" ]; then | |
| REMOTE_HITS="${REMOTE_HITS}${repo}:"$'\n'"${rem}"$'\n\n' | |
| fi | |
| # also check if the attacker commits are reachable in any local clone | |
| for c in "$BAD_COMMIT" "$BAD_COMMIT_FORK"; do | |
| if git -C "$repo" cat-file -e "$c^{commit}" 2>/dev/null; then | |
| REMOTE_HITS="${REMOTE_HITS}${repo}: contains attacker commit $c"$'\n\n' | |
| fi | |
| done | |
| done < <(find "$base" -maxdepth 4 -type d -name .git 2>/dev/null) | |
| done | |
| if [ -n "$REMOTE_HITS" ]; then | |
| bad "git remote/commit references attacker account or commit" | |
| printf '%s' "$REMOTE_HITS" | sed 's/^/ /' | |
| else | |
| ok "no git remotes/commits reference attacker accounts" | |
| fi | |
| # ---------- summary ---------- | |
| printf '\n%s════ Result ════%s\n' "$BLD" "$RST" | |
| if [ "$HITS" -eq 0 ]; then | |
| cat <<EOF | |
| ${GRN}${BLD}CLEAN${RST} — no Mini Shai-Hulud IOCs found on this host. | |
| ${DIM}This script covers the publicly disclosed IOCs as of 2026-05-13. | |
| Active campaigns evolve — paste this output into Claude/Codex and ask | |
| for a second opinion if you're unsure.${RST} | |
| verdict: CLEAN | |
| EOF | |
| else | |
| printf '%s%sCOMPROMISE INDICATORS FOUND%s — %d hit(s):\n\n' "$RED" "$BLD" "$RST" "$HITS" | |
| for f in "${FINDINGS[@]}"; do printf ' • %s\n' "$f"; done | |
| cat <<EOF | |
| ${YLW}What to do RIGHT NOW:${RST} | |
| 1. Disconnect this machine from the network (Wi-Fi off, unplug ethernet). | |
| 2. Do NOT type any passwords on this machine. | |
| 3. Rotate from a CLEAN device, in priority order: | |
| - npm tokens | |
| - GitHub PATs and OIDC trust grants | |
| - AWS keys + instance roles | |
| - Vault / Kubernetes service-account tokens | |
| - SSH keys + signing keys | |
| 4. Block egress to filev2.getsession.org, seed{1,2,3}.getsession.org, | |
| git-tanstack.com, api.masscan.cloud, litter.catbox.moe, and 83.142.209.194. | |
| 5. Audit recent GitHub commits and npm publishes from your org. | |
| 6. Paste this ENTIRE output into Claude or Codex (from a clean device) and ask: | |
| "Am I compromised by Mini Shai-Hulud / the TanStack npm attack? | |
| Here is my scan output: <paste>" | |
| References: | |
| - https://openai.com/index/our-response-to-the-tanstack-npm-supply-chain-attack/ | |
| - https://socket.dev/blog/tanstack-npm-packages-compromised-mini-shai-hulud-supply-chain-attack | |
| - https://tanstack.com/blog/npm-supply-chain-compromise-postmortem | |
| - https://docs.mistral.ai/resources/security-advisories | |
| - GitHub Security Advisory: GHSA-g7cv-rxg3-hmpx (TanStack) | |
| - GitHub Security Advisory: GHSA-jgg6-4rpr-wfh7 (Mistral npm) | |
| - GitHub Security Advisory: GHSA-wx9m-wx4f-4cmg (Mistral PyPI) | |
| verdict: SUSPECT | |
| EOF | |
| fi | |
| exit 0 |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment