{"uuid": "c335ab70-dd97-4524-9da5-d76224cbfe6f", "vulnerability_lookup_origin": "1a89b78e-f703-45f3-bb86-59eb712668bd", "author": "9f56dd64-161d-43a6-b9c3-555944290a09", "vulnerability": "GHSA-g7cv-rxg3-hmpx", "type": "seen", "source": "https://gist.github.com/danw-relaytech/d18d8484b1f37e6e84349fe1e8e49732", "content": "#!/usr/bin/env bash\n# tanstack_check.sh \u2014 local detection for the 2026-05-11 TanStack npm compromise\n# (a.k.a. \"Mini Shai-Hulud\", GHSA-g7cv-rxg3-hmpx).\n#\n# Run on dev laptops. Exit code 0 = clean, 1 = hit, 2 = some checks skipped.\n# Window: 2026-05-11 19:20-21:30 UTC (epoch 1778527200-1778535000)\n#\n# Sources / further reading:\n#   - TanStack postmortem:  https://tanstack.com/blog/npm-supply-chain-compromise-postmortem\n#   - GH advisory:          https://github.com/TanStack/router/security/advisories/GHSA-g7cv-rxg3-hmpx\n#   - GH tracking issue:    https://github.com/TanStack/router/issues/7383\n#   - HN thread (IOCs):     https://news.ycombinator.com/item?id=48100706\n#   - StepSecurity writeup: https://www.stepsecurity.io/blog/mini-shai-hulud-is-back-a-self-spreading-supply-chain-attack-hits-the-npm-ecosystem\n#   - Aikido writeup:       https://www.aikido.dev/blog/mini-shai-hulud-is-back-tanstack-compromised\n#   - Wiz writeup:          https://www.wiz.io/blog/mini-shai-hulud-strikes-again-tanstack-more-npm-packages-compromised\n#   - Snyk writeup:         https://snyk.io/blog/tanstack-npm-packages-compromised/\n#\n# IMPORTANT: If anything below flags as compromised, do NOT rotate your GitHub\n# token until the gh-token-monitor LaunchAgent/systemd unit (check 6 below) has\n# been removed. The malware polls api.github.com/user every 60s with the stolen\n# token; on revocation it executes `rm -rf ~/`. Source: HN/StepSecurity.\n\nset -u\n\nUSER_ID=\"${USER:-$(whoami)}\"\nHOST_ID=\"$(hostname)\"\necho \"=== tanstack_check.sh ===\"\necho \"user:    $USER_ID\"\necho \"host:    $HOST_ID\"\necho \"date:    $(date -u +%Y-%m-%dT%H:%M:%SZ)\"\necho\n\nhits=0\nskips=0\n\n# Early exit: if no JS package manager or runtime is installed, the install-time\n# payload had no way to execute. We still scan for in-repo persistence artifacts\n# (.claude/, .vscode/) because those could land via `git clone` of a poisoned\n# repo, but the host-level credential-theft vector requires node/npm/pnpm/yarn/bun.\nHAS_JS_TOOLING=0\nfor bin in node npm pnpm yarn bun; do\n  if command -v \"$bin\" &gt;/dev/null 2&gt;&amp;1; then\n    HAS_JS_TOOLING=1\n    break\n  fi\ndone\nif [ \"$HAS_JS_TOOLING\" -eq 0 ]; then\n  echo \"No JS package manager or runtime found (node/npm/pnpm/yarn/bun).\"\n  echo \"Install-time credential theft was not possible on this host.\"\n  echo \"RESULT: not applicable.\"\n  exit 0\nfi\n\n# Known-bad payload hashes (Aikido / StepSecurity / Wiz)\nBAD_SHA_ROUTER_INIT=\"ab4fcadaec49c03278063dd269ea5eef82d24f2124a8e15d7b90f2fa8601266c\"\nBAD_SHA_TANSTACK_RUNNER=\"2ec78d556d696e208927cc503d48e4b5eb56b31abc2870c2ed2e98d6be27fc96\"\nBAD_SHA_SETUP_PACKAGE=\"7c12d8614c624c70d6dd6fc2ee289332474abaa38f70ebe2cdef064923ca3a9b\"\nBAD_SHA_SETUP_MJS=\"2258284d65f63829bd67eaba01ef6f1ada2f593f9bbe41678b2df360bd90d3df\"\n\n# Full list of malicious @tanstack/* versions from GHSA-g7cv-rxg3-hmpx\n# (42 packages x 2 versions = 84 malicious versions). Written to a temp file\n# and fed to `grep -E -f` so we don't have to inline a multi-kilobyte regex.\nBAD_TANSTACK_PATTERNS=\"$(mktemp -t tanstack_bad.XXXXXX)\"\ntrap 'rm -f \"$BAD_TANSTACK_PATTERNS\"' EXIT\ncat &gt; \"$BAD_TANSTACK_PATTERNS\" &lt;&lt;'PATTERNS'\n@tanstack/arktype-adapter[\":@ ]+1\\.166\\.(12|15)\n@tanstack/eslint-plugin-router[\":@ ]+1\\.161\\.(9|12)\n@tanstack/eslint-plugin-start[\":@ ]+0\\.0\\.(4|7)\n@tanstack/history[\":@ ]+1\\.161\\.(9|12)\n@tanstack/nitro-v2-vite-plugin[\":@ ]+1\\.154\\.(12|15)\n@tanstack/react-router[\":@ ]+1\\.169\\.(5|8)\n@tanstack/react-router-devtools[\":@ ]+1\\.166\\.(16|19)\n@tanstack/react-router-ssr-query[\":@ ]+1\\.166\\.(15|18)\n@tanstack/react-start[\":@ ]+1\\.167\\.(68|71)\n@tanstack/react-start-client[\":@ ]+1\\.166\\.(51|54)\n@tanstack/react-start-rsc[\":@ ]+0\\.0\\.(47|50)\n@tanstack/react-start-server[\":@ ]+1\\.166\\.(55|58)\n@tanstack/router-cli[\":@ ]+1\\.166\\.(46|49)\n@tanstack/router-core[\":@ ]+1\\.169\\.(5|8)\n@tanstack/router-devtools[\":@ ]+1\\.166\\.(16|19)\n@tanstack/router-devtools-core[\":@ ]+1\\.167\\.(6|9)\n@tanstack/router-generator[\":@ ]+1\\.166\\.(45|48)\n@tanstack/router-plugin[\":@ ]+1\\.167\\.(38|41)\n@tanstack/router-ssr-query-core[\":@ ]+1\\.168\\.(3|6)\n@tanstack/router-utils[\":@ ]+1\\.161\\.(11|14)\n@tanstack/router-vite-plugin[\":@ ]+1\\.166\\.(53|56)\n@tanstack/solid-router[\":@ ]+1\\.169\\.(5|8)\n@tanstack/solid-router-devtools[\":@ ]+1\\.166\\.(16|19)\n@tanstack/solid-router-ssr-query[\":@ ]+1\\.166\\.(15|18)\n@tanstack/solid-start[\":@ ]+1\\.167\\.(65|68)\n@tanstack/solid-start-client[\":@ ]+1\\.166\\.(50|53)\n@tanstack/solid-start-server[\":@ ]+1\\.166\\.(54|57)\n@tanstack/start-client-core[\":@ ]+1\\.168\\.(5|8)\n@tanstack/start-fn-stubs[\":@ ]+1\\.161\\.(9|12)\n@tanstack/start-plugin-core[\":@ ]+1\\.169\\.(23|26)\n@tanstack/start-server-core[\":@ ]+1\\.167\\.(33|36)\n@tanstack/start-static-server-functions[\":@ ]+1\\.166\\.(44|47)\n@tanstack/start-storage-context[\":@ ]+1\\.166\\.(38|41)\n@tanstack/valibot-adapter[\":@ ]+1\\.166\\.(12|15)\n@tanstack/virtual-file-routes[\":@ ]+1\\.161\\.(10|13)\n@tanstack/vue-router[\":@ ]+1\\.169\\.(5|8)\n@tanstack/vue-router-devtools[\":@ ]+1\\.166\\.(16|19)\n@tanstack/vue-router-ssr-query[\":@ ]+1\\.166\\.(15|18)\n@tanstack/vue-start[\":@ ]+1\\.167\\.(61|64)\n@tanstack/vue-start-client[\":@ ]+1\\.166\\.(46|49)\n@tanstack/vue-start-server[\":@ ]+1\\.166\\.(50|53)\n@tanstack/zod-adapter[\":@ ]+1\\.166\\.(12|15)\nPATTERNS\n\n# Scope filesystem searches to every relaytech-co/relaycode checkout under $HOME.\n# Hard-coded paths (~/code, ~/repositories, ~/_git, ...) were too brittle \u2014 devs\n# clone wherever they like \u2014 so we discover roots by finding .git entries at\n# shallow depth and confirming via `git config remote.origin.url`. This handles\n# regular clones, bare clones, and linked worktrees uniformly.\n#\n# Override / extend via env var:\n#   TANSTACK_EXTRA_ROOTS=\"/path/a:/path/b\" ./tanstack_check.sh\nSEARCH_ROOTS=()\nrelay_repo_match() { case \"$1\" in *relaytech-co/relaycode*) return 0;; esac; return 1; }\n\ngit_candidates=\"$(find \"$HOME\" -maxdepth 5 \\\n  \\( -path '*/Library' -o -path '*/.Trash' -o -path '*/Caches' -o -path '*/.cache' \\\n     -o -path '*/node_modules' -o -path '*/.npm' -o -path '*/Applications' \\\n     -o -path '*/.rbenv' -o -path '*/.pyenv' -o -path '*/.nvm' \\\n     -o -path '*/Downloads' \\) -prune \\\n  -o -name '.git' -print 2&gt;/dev/null)\"\nwhile IFS= read -r gitpath; do\n  [ -z \"$gitpath\" ] &amp;&amp; continue\n  repo=\"${gitpath%/.git}\"\n  [ -z \"$repo\" ] &amp;&amp; continue\n  url=\"$(git -C \"$repo\" config --get remote.origin.url 2&gt;/dev/null || true)\"\n  relay_repo_match \"$url\" &amp;&amp; SEARCH_ROOTS+=(\"$repo\")\ndone &lt;&lt;&lt; \"$git_candidates\"\n\n# Include the cwd's git toplevel too, in case the script is run from a checkout\n# stashed outside $HOME or pruned by the find above.\nif cwd_top=\"$(git rev-parse --show-toplevel 2&gt;/dev/null)\" \\\n    &amp;&amp; cwd_url=\"$(git -C \"$cwd_top\" config --get remote.origin.url 2&gt;/dev/null)\" \\\n    &amp;&amp; relay_repo_match \"$cwd_url\"; then\n  case \" ${SEARCH_ROOTS[*]-} \" in *\" $cwd_top \"*) ;; *) SEARCH_ROOTS+=(\"$cwd_top\");; esac\nfi\n\n# Manual additions via TANSTACK_EXTRA_ROOTS (colon-separated, like PATH).\nif [ -n \"${TANSTACK_EXTRA_ROOTS:-}\" ]; then\n  IFS=':' read -r -a _extra_roots &lt;&lt;&lt; \"$TANSTACK_EXTRA_ROOTS\"\n  for r in \"${_extra_roots[@]}\"; do\n    [ -d \"$r\" ] &amp;&amp; SEARCH_ROOTS+=(\"$r\")\n  done\nfi\n\nif [ ${#SEARCH_ROOTS[@]} -gt 0 ]; then\n  echo \"discovered relaycode checkouts:\"\n  printf '  %s\\n' \"${SEARCH_ROOTS[@]}\"\nelse\n  echo \"warning: no relaytech-co/relaycode checkout found under \\$HOME\"\n  echo \"         (set TANSTACK_EXTRA_ROOTS=/path/to/repo if it lives elsewhere)\"\nfi\necho\n\ncheck() {\n  local label=\"$1\"; shift\n  local out\n  out=\"$(\"$@\" 2&gt;/dev/null)\"\n  if [ -n \"$out\" ]; then\n    echo \"[FAIL] $label\"\n    echo \"$out\" | sed 's/^/    /'\n    hits=$((hits+1))\n  else\n    echo \"[ ok ] $label\"\n  fi\n}\n\n###############################################################################\n# 1. Shell history during the malicious publish window\n#    (Source: postmortem timeline; clearable by attacker \u2014 corroborating only)\n###############################################################################\nif [ -r \"$HOME/.zsh_history\" ]; then\n  check \"1. zsh history install during 19:20-21:30 UTC on 2026-05-11\" \\\n    bash -c \"LC_ALL=C awk -F'[:;]' '/^: [0-9]+:0;/ &amp;&amp; \\$2&gt;=1778527200 &amp;&amp; \\$2&lt;=1778535000' \\\"$HOME/.zsh_history\\\" \\\n      | grep -iE 'npm i|pnpm i|yarn (install|add)'\"\nelse\n  echo \"[skip] 1. zsh history (file not readable)\"\n  skips=$((skips+1))\nfi\n\nif [ -r \"$HOME/.bash_history\" ]; then\n  # Bash history with HISTTIMEFORMAT writes `#` on its own line before\n  # each command. Track the most recent timestamp and emit commands that fall\n  # in the malicious-publish window. If HISTTIMEFORMAT was never set, every\n  # line is command-only and t stays 0, so nothing prints (no false negatives;\n  # nothing we can do without timestamps).\n  check \"1b. bash history install during 19:20-21:30 UTC on 2026-05-11\" \\\n    bash -c \"awk '/^#[0-9]+\\$/ { t = substr(\\$0, 2)+0; next } t&gt;=1778527200 &amp;&amp; t&lt;=1778535000' \\\"$HOME/.bash_history\\\" \\\n      | grep -iE 'npm i|pnpm i|yarn (install|add)'\"\nelse\n  echo \"[skip] 1b. bash history (file not readable)\"\n  skips=$((skips+1))\nfi\n\n###############################################################################\n# 2. Claude Code session activity during the window\n###############################################################################\nif [ -d \"$HOME/.claude/projects\" ]; then\n  check \"2. Claude Code install during window\" \\\n    bash -c \"grep -rhE '\\\"timestamp\\\":\\\"2026-05-11T(19:[2-5][0-9]|20:[0-5][0-9]|21:[0-2][0-9]|21:30)' \\\"$HOME/.claude/projects/\\\" \\\n      | grep -iE 'npm install|pnpm install|yarn install|npm ci'\"\nelse\n  echo \"[skip] 2. Claude Code logs (~/.claude/projects/ not present)\"\n  skips=$((skips+1))\nfi\n\n###############################################################################\n# 3. Malicious @tanstack version pinned in any lockfile / package.json,\n#    plus orphan commit / @tanstack/setup / attacker handle fingerprints in\n#    both lockfiles AND installed node_modules/@tanstack/ manifests.\n#    (Sources: GHSA-g7cv-rxg3-hmpx, Snyk)\n###############################################################################\nif [ ${#SEARCH_ROOTS[@]} -gt 0 ]; then\n  check \"3. malicious @tanstack version pinned in lockfile/package.json (all 42 pkgs)\" \\\n    bash -c \"find \\\"${SEARCH_ROOTS[@]}\\\" \\\n      \\\\( -name 'node_modules' -o -name '.git' \\\\) -prune -o \\\n      \\\\( -name 'package.json' -o -name 'package-lock.json' -o -name 'pnpm-lock.yaml' -o -name 'yarn.lock' \\\\) -print 2&gt;/dev/null \\\n      | xargs grep -lE -f \\\"$BAD_TANSTACK_PATTERNS\\\" 2&gt;/dev/null\"\n\n  # 3b. Orphan-commit fingerprint / fictitious @tanstack/setup / attacker handle\n  #     in lockfiles or package.json files (excluding node_modules)\n  check \"3b. malicious @tanstack/setup, orphan commit ref, or attacker handle in lockfiles\" \\\n    bash -c \"find \\\"${SEARCH_ROOTS[@]}\\\" \\\n      \\\\( -name 'node_modules' -o -name '.git' \\\\) -prune -o \\\n      \\\\( -name 'package.json' -o -name 'package-lock.json' -o -name 'pnpm-lock.yaml' -o -name 'yarn.lock' \\\\) -print 2&gt;/dev/null \\\n      | xargs grep -lE '79ac49eedf774dd4b0cfa308722bc463cfe5885c|@tanstack/setup|voicproducoes|zblgg' 2&gt;/dev/null\"\n\n  # 3c. Same fingerprints but in INSTALLED node_modules/@tanstack/*/package.json\n  #     (Source: Snyk). This is the strongest signal \u2014 a malicious manifest on\n  #     disk in node_modules proves the install actually completed.\n  check \"3c. malicious markers in installed node_modules/@tanstack/*/package.json\" \\\n    bash -c \"find \\\"${SEARCH_ROOTS[@]}\\\" -type d -name '@tanstack' -path '*/node_modules/*' 2&gt;/dev/null \\\n      | xargs -I{} find {} -name 'package.json' 2&gt;/dev/null \\\n      | xargs grep -lE '79ac49eedf774dd4b0cfa308722bc463cfe5885c|@tanstack/setup|voicproducoes|zblgg' 2&gt;/dev/null\"\nelse\n  echo \"[skip] 3. no common code dirs found\"\n  skips=$((skips+3))\nfi\n\n###############################################################################\n# 4. router_init.js / tanstack_runner.js / router_runtime.js / vite_setup.mjs\n#    payload files anywhere on disk; SHA-256 match against known-bad hashes.\n#    (Source: Aikido, StepSecurity, postmortem)\n#    Uses Spotlight (mdfind) on macOS for instant filesystem-wide search.\n###############################################################################\nPAYLOAD_NAMES=(router_init.js tanstack_runner.js router_runtime.js vite_setup.mjs opensearch_init.js)\nPAYLOAD_FILES=\"\"\n# Strategy:\n#   - When mdfind is present: trust Spotlight for global coverage, then run a\n#     targeted find ONLY over SEARCH_ROOTS as a backstop for unindexed code dirs\n#     (e.g. users who excluded ~/code from Spotlight). Avoids find-over-$HOME,\n#     which is too slow for a fleet script.\n#   - When mdfind is absent (Linux): find over $HOME + /tmp + SEARCH_ROOTS.\nif command -v mdfind &gt;/dev/null 2&gt;&amp;1; then\n  for name in \"${PAYLOAD_NAMES[@]}\"; do\n    found=\"$(mdfind -name \"$name\" 2&gt;/dev/null)\"\n    [ -n \"$found\" ] &amp;&amp; PAYLOAD_FILES=\"$PAYLOAD_FILES\"$'\\n'\"$found\"\n  done\n  FIND_ROOTS=()\n  [ ${#SEARCH_ROOTS[@]} -gt 0 ] &amp;&amp; FIND_ROOTS=(\"${SEARCH_ROOTS[@]}\")\nelse\n  FIND_ROOTS=(\"$HOME\" /tmp)\n  [ ${#SEARCH_ROOTS[@]} -gt 0 ] &amp;&amp; for r in \"${SEARCH_ROOTS[@]}\"; do\n    case \"$r\" in \"$HOME\"/*) ;; *) FIND_ROOTS+=(\"$r\") ;; esac\n  done\nfi\nif [ ${#FIND_ROOTS[@]} -gt 0 ]; then\n  found_via_find=\"$(find \"${FIND_ROOTS[@]}\" \\\n    \\( -path '*/Library' -o -path '*/.git' -o -path '*/Caches' -o -name '.Trash' \\) -prune \\\n    -o -type f \\( -name 'router_init.js' -o -name 'tanstack_runner.js' \\\n              -o -name 'router_runtime.js' -o -name 'vite_setup.mjs' \\\n              -o -name 'opensearch_init.js' \\) -print 2&gt;/dev/null)\"\n  [ -n \"$found_via_find\" ] &amp;&amp; PAYLOAD_FILES=\"$PAYLOAD_FILES\"$'\\n'\"$found_via_find\"\nfi\nPAYLOAD_FILES=\"$(echo \"$PAYLOAD_FILES\" | sed '/^$/d' | sort -u)\"\n\nif [ -n \"$PAYLOAD_FILES\" ]; then\n  echo \"[FAIL] 4. payload file(s) found on disk \u2014 hashing for IOC match:\"\n  while IFS= read -r f; do\n    [ -z \"$f\" ] &amp;&amp; continue\n    sha=\"$(shasum -a 256 \"$f\" 2&gt;/dev/null | awk '{print $1}')\"\n    tag=\"[unknown sha]\"\n    case \"$sha\" in\n      \"$BAD_SHA_ROUTER_INIT\")    tag=\"[!!! KNOWN-BAD router_init.js]\" ;;\n      \"$BAD_SHA_TANSTACK_RUNNER\") tag=\"[!!! KNOWN-BAD tanstack_runner.js]\" ;;\n      \"$BAD_SHA_SETUP_PACKAGE\")   tag=\"[!!! KNOWN-BAD @tanstack/setup pkg]\" ;;\n    esac\n    echo \"    $tag  $sha  $f\"\n  done &lt;&lt;&lt; \"$PAYLOAD_FILES\"\n  hits=$((hits+1))\nelse\n  echo \"[ ok ] 4. no router_init.js / tanstack_runner.js / router_runtime.js / vite_setup.mjs / opensearch_init.js on disk\"\nfi\n\n###############################################################################\n# 4b. setup.mjs SHA check (Source: Wiz).\n#     setup.mjs is too common a filename to flag on presence alone, so we\n#     enumerate every setup.mjs under SEARCH_ROOTS (including node_modules,\n#     where the malicious copy lives) and only flag SHA-256 matches against\n#     the known-bad hash 2258284d65f63829bd67eaba01ef6f1ada2f593f9bbe41678b2df360bd90d3df.\n###############################################################################\nif [ ${#SEARCH_ROOTS[@]} -gt 0 ]; then\n  # Pre-filter by exact byte size (malicious setup.mjs = 5047 bytes per Wiz).\n  # This avoids hashing every setup.mjs in every node_modules tree, which is\n  # too slow for a fleet script. `-size 5047c` is portable across BSD/GNU find.\n  SETUP_MJS_FILES=\"$(find \"${SEARCH_ROOTS[@]}\" -name '.git' -prune -o \\\n    -type f -name 'setup.mjs' -size 5047c -print 2&gt;/dev/null)\"\n  bad_setup_found=0\n  while IFS= read -r f; do\n    [ -z \"$f\" ] &amp;&amp; continue\n    sha=\"$(shasum -a 256 \"$f\" 2&gt;/dev/null | awk '{print $1}')\"\n    if [ \"$sha\" = \"$BAD_SHA_SETUP_MJS\" ]; then\n      [ \"$bad_setup_found\" -eq 0 ] &amp;&amp; echo \"[FAIL] 4b. setup.mjs with known-bad SHA found:\"\n      echo \"    [!!! KNOWN-BAD setup.mjs]  $sha  $f\"\n      bad_setup_found=1\n    fi\n  done &lt;&lt;&lt; \"$SETUP_MJS_FILES\"\n  if [ \"$bad_setup_found\" -eq 0 ]; then\n    echo \"[ ok ] 4b. no setup.mjs files match known-bad SHA\"\n  else\n    hits=$((hits+1))\n  fi\nelse\n  echo \"[skip] 4b. no common code dirs (setup.mjs SHA check)\"\n  skips=$((skips+1))\nfi\n\n###############################################################################\n# 5. In-repo persistence: .claude/ and .vscode/ artifacts that survive npm uninstall\n#    (Source: StepSecurity)\n#    Looks for SessionStart hook in .claude/settings.json, folderOpen task in\n#    .vscode/tasks.json, and any setup.mjs / router_runtime.js in those dirs.\n###############################################################################\nif [ ${#SEARCH_ROOTS[@]} -gt 0 ]; then\n  check \"5. persistence files in .claude/ or .vscode/ inside repos\" \\\n    bash -c \"find \\\"${SEARCH_ROOTS[@]}\\\" \\\n      \\\\( -name 'node_modules' -o -name '.git' \\\\) -prune -o \\\n      -type f \\\\( -name 'setup.mjs' -o -name 'router_runtime.js' \\\\) \\\n      \\\\( -path '*/.claude/*' -o -path '*/.vscode/*' \\\\) -print 2&gt;/dev/null\"\n\n  check \"5b. SessionStart hook in .claude/settings.json or folderOpen task in .vscode/tasks.json\" \\\n    bash -c \"find \\\"${SEARCH_ROOTS[@]}\\\" \\\n      \\\\( -name 'node_modules' -o -name '.git' \\\\) -prune -o \\\n      \\\\( -path '*/.claude/settings.json' -o -path '*/.vscode/tasks.json' \\\\) -print 2&gt;/dev/null \\\n      | xargs grep -lE 'SessionStart|folderOpen|setup\\\\.mjs|router_runtime' 2&gt;/dev/null\"\n\n  # 5c. Injected codeql_analysis.yml that exfiltrates secrets\n  check \"5c. suspicious .github/workflows/codeql_analysis.yml (injected by worm)\" \\\n    bash -c \"find \\\"${SEARCH_ROOTS[@]}\\\" \\\n      \\\\( -name 'node_modules' \\\\) -prune -o \\\n      -path '*/.github/workflows/codeql_analysis.yml' -print 2&gt;/dev/null \\\n      | xargs grep -lE 'masscan|getsession|git-tanstack|secrets\\\\.toJSON|toJson\\\\(secrets' 2&gt;/dev/null\"\nelse\n  echo \"[skip] 5. no common code dirs (in-repo persistence checks)\"\n  skips=$((skips+3))\nfi\n\n###############################################################################\n# 6. gh-token-monitor dead-man's switch (host-level persistence)\n#    (Source: HN thread / StepSecurity)\n#    Polls GitHub every 60s; on token revocation, runs `rm -rf ~/`.\n#    THIS MUST BE REMOVED *BEFORE* ANY CREDENTIAL ROTATION.\n###############################################################################\ncheck \"6. ~/.local/bin/gh-token-monitor.sh dead-man's switch script\" \\\n  bash -c \"[ -e \\\"$HOME/.local/bin/gh-token-monitor.sh\\\" ] &amp;&amp; echo \\\"$HOME/.local/bin/gh-token-monitor.sh\\\"\"\n\ncheck \"6b. ~/.config/gh-token-monitor/ token storage dir\" \\\n  bash -c \"[ -d \\\"$HOME/.config/gh-token-monitor\\\" ] &amp;&amp; echo \\\"$HOME/.config/gh-token-monitor\\\"\"\n\nif [ \"$(uname)\" = \"Darwin\" ]; then\n  check \"6c. macOS LaunchAgent com.user.gh-token-monitor.plist\" \\\n    bash -c \"ls \\\"$HOME/Library/LaunchAgents/\\\" 2&gt;/dev/null | grep -iE 'gh-token-monitor|com\\\\.user\\\\.gh' || launchctl list 2&gt;/dev/null | grep -i 'gh-token-monitor'\"\nelse\n  check \"6c. Linux systemd user service gh-token-monitor\" \\\n    bash -c \"ls \\\"$HOME/.config/systemd/user/\\\" 2&gt;/dev/null | grep -i 'gh-token-monitor' || systemctl --user list-unit-files 2&gt;/dev/null | grep -i 'gh-token-monitor'\"\nfi\n\n###############################################################################\n# 7. Malicious npm token (literal smoking gun)\n#    (Source: StepSecurity)\n###############################################################################\nif command -v npm &gt;/dev/null 2&gt;&amp;1; then\n  check \"7. malicious npm token 'IfYouRevokeThisTokenItWillWipeTheComputerOfTheOwner'\" \\\n    bash -c \"npm token list 2&gt;/dev/null | grep -i 'IfYouRevokeThisToken'\"\nelse\n  echo \"[skip] 7. npm not installed\"\n  skips=$((skips+1))\nfi\n\n###############################################################################\n# 8. Shell rc modifications (malware overrides `sudo` to capture password)\n#    (Source: HN thread)\n###############################################################################\ncheck \"8. shell rc files modified to override 'sudo' or reference gh-token-monitor\" \\\n  bash -c \"grep -lE 'function sudo|gh-token-monitor' \\\n    \\\"$HOME/.bashrc\\\" \\\"$HOME/.bash_profile\\\" \\\"$HOME/.zshrc\\\" \\\"$HOME/.zprofile\\\" \\\"$HOME/.profile\\\" 2&gt;/dev/null\"\n\n###############################################################################\n# 9. Outbound network IOCs \u2014 DNS resolver cache + recent unified log (24h)\n#    (Source: postmortem IOC list \u2014 Session network + Aikido/StepSecurity C2)\n#    The full `log show --last 7d` is too slow for a fleet script; we use a\n#    24h window and require an opt-in flag (TANSTACK_DEEP_LOG=1) to go longer.\n###############################################################################\nif [ \"$(uname)\" = \"Darwin\" ] &amp;&amp; command -v log &gt;/dev/null 2&gt;&amp;1; then\n  WINDOW=\"${TANSTACK_DEEP_LOG:+7d}\"; WINDOW=\"${WINDOW:-24h}\"\n  out=\"$(log show --last \"$WINDOW\" --style compact 2&gt;/dev/null \\\n    | grep -iE 'getsession\\.org|catbox\\.moe|git-tanstack\\.com|masscan\\.cloud' \\\n    | head -20)\"\n  if [ -n \"$out\" ]; then\n    echo \"[FAIL] 9. macOS unified log ($WINDOW) mentions IOC domain:\"\n    echo \"$out\" | sed 's/^/    /'\n    hits=$((hits+1))\n  else\n    echo \"[ ok ] 9. no IOC domains in last $WINDOW of macOS unified log\"\n  fi\nelse\n  echo \"[skip] 9. macOS unified log not available\"\n  skips=$((skips+1))\nfi\n\n###############################################################################\n# 10. Suspicious bun / python3 process reading another process's memory\n#     (Source: StepSecurity, runner-memory OIDC theft technique)\n###############################################################################\n# Require a `bun`/`node` interpreter prefix on the payload-name match so\n# searches like `find ... router_init.js` don't false-positive.\ncheck \"10. running bun/node process executing tanstack/opensearch payload, or gh-token-monitor\" \\\n  bash -c \"ps -axww -o command= 2&gt;/dev/null \\\n    | grep -iE '(bun|node|deno)[^|]*(tanstack_runner|router_init|opensearch_init)|gh-token-monitor' \\\n    | grep -vE 'grep|find|tanstack_check'\"\n\n###############################################################################\necho\necho \"=== summary ===\"\necho \"user:  $USER_ID\"\necho \"host:  $HOST_ID\"\necho \"hits:  $hits\"\necho \"skips: $skips\"\n\nif [ \"$hits\" -gt 0 ]; then\n  cat &lt;&lt;'BANNER'\nRESULT: POTENTIAL COMPROMISE.\n  1. DISCONNECT from network.\n  2. Escalate to platform eng immediately.\nBANNER\n  exit 1\nelif [ \"$skips\" -gt 0 ]; then\n  echo \"RESULT: clean for checks that ran, but $skips check(s) skipped.\"\n  exit 2\nelse\n  echo \"RESULT: clean.\"\n  exit 0\nfi\n", "creation_timestamp": "2026-05-12T16:58:08.000000Z"}