A simple project to detect the Shai-Hulud npm supply chain attack
Shell
410
268 commits
updated Oct 12, 2026
A Bash script that scans a project — or many projects at once — for known traces of the September 2025 → October 2026 npm, PyPI, Composer, Crates, Go, Hex, and RubyGems supply-chain attacks. Cross-checks 5,791 confirmed bad package versions and a library of content-pattern IoCs (file hashes, C2 domains, dead-man's-switch artifacts, wipe-threat strings, AI-assistant config droppers, etc.).
git clone https://github.com/Cobenian/shai-hulud-detect
cd shai-hulud-detect
chmod +x shai-hulud-detector.sh
# Scan one project
./shai-hulud-detector.sh /path/to/your/project
# Scan every project under one or more parent dirs and get one aggregate report
./shai-hulud-detector.sh --bulk ~/dev ~/work
# Save findings to a file (same format as CI-friendly logs)
./shai-hulud-detector.sh --save-log report.log /path/to/project
Exit codes (drop straight into CI): 0 clean · 1 high-risk · 2 medium-risk · 3 per-project scan errored (bulk mode only).
The detector looks for two kinds of evidence on disk:
package.json, lockfile, pyproject.toml, requirements.txt, Pipfile, poetry.lock, uv.lock, etc. is parsed and the resolved versions are checked against the 5,791-entry list in compromised-packages.txt. Transitive deps inside node_modules/ are checked too, not skipped.| Wave | Date | Scope |
|---|---|---|
| Chalk/Debug crypto theft | 2025-09-08 | 18+ packages, ~2B weekly downloads |
| Shai-Hulud worm | 2025-09-14 | 517+ packages (@ctrl, @crowdstrike, …) |
| Shai-Hulud "Second Coming" (fake Bun) | 2025-11-24 | 1,100+ packages |
| Golden Path variant | 2025-12-28 | renamed Bun-attack files |
| SANDWORM_MODE workflow poisoning | 2026-02-17 | 19 packages + GitHub Action |
| Axios RAT compromise | 2026-03-31 | [email protected]/0.30.4 + plain-crypto-js |
| Mini Shai-Hulud / TanStack | 2026-05-11 | 400+ versions, dead-man's-switch |
| Megalodon (GitHub-repo backdooring) | 2026-05-18 | 5,561 repos via stolen PATs; @tiledesk/[email protected] as npm fallout |
| Mini Shai-Hulud / AntV (atool) | 2026-05-19 | 643 versions, 323 packages |
| Web3 / DeFi MCP-server typosquat | 2026-05-20 | 10 packages (chain-key-validator, defi-threat-scanner, …), exfiltrates SSH + wallet keys |
| Polymarket wallet drainer | 2026-05-21 | 9 packages from polymarketdev (polymarket-bot, polymarket-trader, …), fake wallet-onboarding prompt captures private keys |
| Bitwarden CLI ("Third Coming") | 2026-04-22 | @bitwarden/[email protected] via Checkmarx ast-github-action breach; bw1.js exfil to audit.checkmarx.cx |
| node-ipc backdoor | 2026-05-14 | [email protected]/9.2.3/12.0.1; IIFE in node-ipc.cjs, DNS exfil to sh.azurestaticprovider.net |
| Nx Console VS Code ext | 2026-05-18 | [email protected]; payload from orphan commit 558b09d7 in nrwl/nx, targets ~/.claude/settings.json (TeamPCP / GitHub breach) |
| TrapDoor (TeamPCP) | 2026-05-22→25 | 34 packages / 384+ versions across npm + PyPI + Crates; plants .cursorrules/CLAUDE.md AI-assistant droppers |
| Laravel-Lang tag-rewrite | 2026-05-22 | 700+ Composer tags force-rewritten (laravel-lang/lang, …); RCE on autoload, DebugElevator stealer |
| mouse5212 "Malware-Slop" | 2026-05-26 | mouse5212-super-formatter exfils Claude's /mnt/user-data via embedded GitHub PAT (unplowed3584) |
| art-template npm hijack | 2025-03 → 2026-05 | 4 versions ([email protected]), iOS browser exploit kit (UNC6691) |
| sl4x0 dependency confusion | 2025-06 → 2026-03 | 92+ packages across 32 *poc accounts, DNS exfil to oob.sl4x0.xyz (likely security research) |
durabletask PyPI worm | 2026-05-19 | pypi:durabletask:1.4.1-1.4.3, multi-cloud credential stealer + AWS SSM / k8s lateral movement |
| PyPI cross-spread (TeamPCP) | 2026-03 → 05 | litellm, telnyx, xinference, lightning, mistralai, guardrails-ai |
| Miasma "Phantom Gyp" worm | 2026-06-03 | 57 packages / 286 versions (@vapi-ai/server-sdk, ai-sdk-ollama, autotel-*, awaitly-*, executable-stories-*); novel binding.gyp command-substitution trigger bypasses preinstall-script monitors |
| Miasma "Hades" PyPI wave | 2026-06-07 | 19 PyPI packages / 37 versions (bramin, magique-ai, pantheon-agents, executor-engine, ufish); novel *-setup.pth Python startup-hook execs an _index.js loader on plain import, no install hook needed; C2 camouflaged under api.anthropic.com |
| IronWorm ("rustier cousin") | 2026-06-03 | 37 npm packages from the asteroiddao account (weavedb-*, arnext, cwao, wao, zkjson); Rust infostealer + eBPF rootkit via preinstall ELF hook, Exodus-wallet theft, leaked operator wallet 0x7e28…a4d6 |
| easy-day-js / Mastra AI | 2026-06-17 | 141 @mastra/* packages + mastra/create-mastra republished with an injected easy-day-js (dayjs typosquat) dependency; postinstall dropper disables TLS, pulls a cross-platform infostealer from C2 23.254.164.92/.123; North-Korea-attributed (Sapphire Sleet / BlueNoroff) |
| Miasma LeoPlatform / RStreams | 2026-06-25 | 23 npm packages (leo-*, rstreams-*, serverless-leo, …); Miasma worm reaches the AWS data-streaming ecosystem via the binding.gyp trigger + editor/CI auto-run hooks; new markers RevokeAndItGoesKaboom, thebeautifulmarchoftime |
| Shai-Hulud "Here We Go Again" (keyv / cacheable) — also tracked as ChainDrop (Microsoft, Elastic) | 2026-08-04 | Largest wave by install volume: 444 npm packages / 2,236 versions across 15 scopes, plus 3 Go modules / 82 versions, >2B monthly installs (keyv, flat-cache, file-entry-cache, cacheable-request, cacheable, cache-manager, @servicetitan/*, @ornikar/*, @onereach/*, @or-sdk/*, @qlik/*, @nebula.js/*, @umacloud/*); "preinstall": "node setup.mjs" bootstraps Bun 1.3.13 → 727KB Math_Symbol.js; primary exfil is to GitHub dead-drop repos described Shai-Hulud: Here We Go Again, with no hard-coded C2 — the payload reads its live domain from Ethereum contract 0xE1f2395e… at run time, so hosts rotate without republishing (npm-cache[.]com → awqhnjewqjkl[.]icu on Aug 4); poisoned releases carry valid npm OIDC provenance / SLSA attestation |
| Mini Shai-Hulud resurfacing | 2026-09-07 | 4 npm packages / 4 versions (blueai-cli, bmc-i18n-extract-cli, bmc-translate-utils, feishu-docx-mcp), all published by one account within an hour; the byte-identical May 19 @antv payload (index.js, hash in MALICIOUS_HASHLIST) via "preinstall": "bun run index.js", 111 days after it was last seen |
| "supplychain.local" Go-binary worm | 2026-09-23 | A new family, not Shai-Hulud code: @memtensor/[email protected]/0.1.23/0.1.25 (the even versions in between are clean maintainer republishes) and pypi:[email protected]. A multiplatform Go sckit binary (6 hashes in MALICIOUS_HASHLIST) steals tokens and republishes through npm, PyPI and a planted GitHub Actions workflow. It runs on invocation, not at install |
| tensorlake (ChainDrop family) | 2026-10-08 | [email protected], published by the project's own release workflow with provenance after an attacker commit to main; "preinstall": "node lib/setup.mjs" → Bun → lib/Math_Symbol.js (both hashed). Same payload family as the keyv wave but new infrastructure: C2 iseekaigogo[.]com, rotation contract 0xb614155F…, operator wallet 0x779f83aE…, and the WORMTAG infection-tag variable, all matched |
For per-wave IoC inventories, payload hashes, source advisories, and version-by-version lists, see CHANGELOG.md.
⚠️ Dead-man's-switch warning
Two waves (May 11 and May 19, 2026) install a persistence daemon (
gh-token-monitor/kitty-monitor) that wipes the host if its monitored GitHub token is revoked. Run with--check-hostto detect the persistence artifacts. If they're present, stop and remove the service BEFORE rotating any tokens. The console summary and aggregate report both print a safe remediation order.
| Ecosystem | Auto-detected via | Status |
|---|---|---|
| npm | package.json, package-lock.json, yarn.lock, pnpm-lock.yaml | Full support |
| PyPI | pyproject.toml, requirements*.txt, Pipfile, Pipfile.lock, poetry.lock, uv.lock, setup.py, setup.cfg | Full support (pure-bash awk parsers, no Python required) |
| Composer | composer.json, composer.lock | Full support (PHP / Packagist; added for the Laravel-Lang wave) |
| Crates | Cargo.toml, Cargo.lock | Full support (Rust / crates.io; added for the TrapDoor wave) |
| Go | go.mod, go.sum | Full support (Go modules; added for the Miasma LeoPlatform wave. Versions keep their canonical leading v) |
| Hex | mix.exs, mix.lock | Full support (Elixir / Hex.pm; mix.lock is authoritative, mix.exs requirements are best-effort) |
| Gem | Gemfile, Gemfile.lock | Full support (RubyGems / Bundler; Gemfile.lock specs are authoritative, Gemfile requirements are best-effort) |
Auto-detection looks for marker files in your tree, skipping node_modules/, vendor/, .venv/, venv/, .tox/, site-packages/, dist/, build/, and similar trees. That exclusion only decides which checks to run — content inside node_modules/ is still fully scanned for compromised versions and malware indicators. Override auto-detection with --ecosystem=npm, --ecosystem=pypi, --ecosystem=all, or a comma-separated list.
- name: Shai-Hulud scan
run: |
chmod +x ./shai-hulud-detector.sh
./shai-hulud-detector.sh --save-log shai-hulud-report.log .
# Job fails on exit 1 (high-risk) or 2 (medium-risk)
- uses: actions/upload-artifact@v4
if: always()
with:
name: shai-hulud-report
path: shai-hulud-report.log
For GitLab CI, Jenkins, and a custom exit-code handler in shell, see docs/ci-examples.md — or just call the script and switch on $?, the exit-code contract is the same everywhere.
--bulk)./shai-hulud-detector.sh --bulk ~/dev ~/work # auto-discover all projects
./shai-hulud-detector.sh --bulk --paranoid --bulk-output ./audit ~/dev
./shai-hulud-detector.sh --bulk --bulk-list ~/dev # dry-run: print what would be scanned
Project discovery is content-aware: a directory with .git, a package.json, a pyproject.toml, Cargo.toml, go.mod, etc. is one scan unit (monorepos stay whole). "Bucket" folders like ~/dev/clients/<client>/<project> are descended into. node_modules, vendor, dist, build dirs, and hidden dirs are never entered. The detector's own repo is skipped automatically. Unreadable directories are reported instead of silently skipped.
Output goes to ./shai-hulud-bulk-report-<timestamp>/ (or --bulk-output DIR):
shai-hulud-bulk-report-<timestamp>/
├── aggregate-report.md # summary tables + per-project results + remediation
└── per-repo/
├── <project>.findings.log # severity-grouped file paths
└── <project>.console.txt # full scan output, ANSI-stripped
--paranoid)Adds typosquatting detection and network-exfiltration heuristics on top of the core checks. These are general-purpose security signals, not Shai-Hulud-specific, and produce more false positives — useful for audits, not recommended for CI gating.
--check-host)Walks $HOME for the May 2026 dead-man's-switch artifacts (gh-token-monitor, kitty-monitor). Off by default. See the warning above before remediating any findings.
--save-log FILE)Writes flagged file paths grouped by severity, in a format friendly to grep and CI artifact uploads:
# HIGH
/path/to/router_init.js
/path/to/package.json
# MEDIUM
/path/to/suspicious-content.js
# LOW
/path/to/namespace-warning.json
--json FILE)For CI gates and tooling that consume findings as data (rather than parsing console text), --json writes a structured report. It uses the same severity tiers as --save-log but keeps the reason for each finding (which --save-log drops):
./shai-hulud-detector.sh --json report.json /path/to/project
{
"schema_version": "1.0",
"tool": "shai-hulud-detector",
"tool_version": "3.9.0",
"generated_at": "2026-06-26T00:00:00Z",
"scan_path": "/path/to/project",
"summary": { "high": 7, "medium": 0, "low": 0 },
"risk_level": "high",
"findings": [
{ "severity": "HIGH", "file": "package.json", "line": 6, "message": "[email protected]" }
]
}
line is a best-effort pointer at the dependency line in the manifest for package-shaped findings (name@version, @scope/name@version); it is null for prose findings or when no match is found. The exit-code contract (0/1/2) is unchanged and remains the authoritative CI signal — --json and --save-log can be combined.
--jsonis the only mode that requiresjq; it fails fast with a clear message ifjqis missing. The default text output keeps the tool's zero-runtime-dependency guarantee.
| Flag | Effect |
|---|---|
--json FILE | Write findings as structured JSON (severity/file/line/message + summary). Requires jq. |
--check-semver-ranges | Flag ^/~ ranges that could resolve to compromised versions (informational, LOW risk). |
--ecosystem LIST | Restrict checks to npm, pypi, all, or a comma-separated list. Default: auto-detect. |
--parallelism N | Threads for parallelisable steps. Defaults to your CPU count. |
--use-git-grep / --use-ripgrep / --use-grep | Force a specific grep tool. Default: auto-select fastest available. |
--bulk-depth N | Depth cap for bulk discovery (default 3). --bulk-depth 1 = flat. |
--bulk-list | With --bulk: print what would be scanned and exit. |
--bulk-output DIR | Where to write the bulk report (default ./shai-hulud-bulk-report-<timestamp>/). |
find pass, categorized by extension).compromised-packages.txt via a sorted set-intersection (comm -12).bundle.js, setup_bun.js, router_init.js, tanstack_runner.js, cat.py, node-ipc.cjs, etc.) and compare against 20 known-malicious SHA-256s.optionalDependencies patterns.$HOME for persistence artifacts (--check-host); run typosquatting + network-exfil heuristics (--paranoid); flag latent semver-range risk (--check-semver-ranges).Detection is read-only. The script never modifies, deletes, or quarantines anything — manual review and remediation are on you.
✅ No indicators of Shai-Hulud compromise detected. ← clean, exit 0
🚨 HIGH RISK: ... ← exit 1, immediate action
⚠️ MEDIUM RISK: ... ← exit 2, manual review
HIGH: definitive indicators — compromised package version, known-malicious file hash, malicious workflow, dead-man's-switch artifact. Stop and remediate.
MEDIUM: suspicious patterns that warrant a look — semver range that could match a compromised version, references to webhook redirector domains, suspicious git branches.
LOW: informational notes — namespace warnings (packages from affected namespaces at safe versions), legitimate-looking patterns that share shape with attack techniques.
mapfile). macOS ships 3.x; install with brew install bash and invoke via /opt/homebrew/bin/bash.find, grep, awk, sed, sort, comm, shasum (or sha256sum), xargs. All POSIX-portable.The script auto-selects the fastest available grep tool (git grep > ripgrep > grep). No runtime dependencies on Python, Node, or anything else.
compromised-packages.txt — new variants need a list update.compromised-packages.txt is a flat text file:
# Comments start with #
axios:1.14.1 # bare entry = npm (back-compat)
npm:@tanstack/react-router:1.169.5 # explicit npm prefix
pypi:mistralai:2.4.6 # PyPI entry
composer:laravel-lang/lang:15.29.5 # Composer / Packagist entry
crates:sui-move-build-helper:0.1.0 # Crates.io / Cargo entry
go:github.com/verana-labs/verana-blockchain:v0.10.1-dev.20 # Go module (version keeps its leading "v")
hex:some_pkg:1.2.3 # Elixir / Hex.pm entry
gem:some_gem:1.2.3 # RubyGems entry
For campaigns where every version of a package is malicious (e.g. TrapDoor, Laravel-Lang's tag rewrite), per-version entries can't keep up — detection is done version-agnostically by a dedicated check_*_indicators function that name-matches the dependency in any manifest.
To add new packages from a fresh advisory: append entries in that format, run ./run-tests.sh, open a PR. Source the additions from a reputable security firm (Socket, StepSecurity, Aikido, Snyk, JFrog, Wiz, Semgrep, SafeDep, GitGuardian, OX Security) and cite them.
./run-tests.sh # full suite, 296 checks
./shai-hulud-detector.sh test-cases/<fixture-name> # run one fixture manually
Each subdirectory of test-cases/ is a self-contained fixture (clean projects, infected projects with the various attack signatures, ecosystem-specific tests, paired clean/dirty pairs). Run ls test-cases/ to see them all.
PRs welcome — especially new compromised-package entries as fresh waves are disclosed.
compromised-packages.txt, add a fixture under test-cases/, register it in run-tests.sh.check_* function in shai-hulud-detector.sh, add an assertion in run-tests.sh../run-tests.sh must pass.Don't include actual malware in test fixtures — inert string constants and synthetic files only.
Detection only. The script does not remove malicious code, downgrade packages, or block installs. Verify findings, then remediate manually.
For the dead-man's-switch waves (May 11 + May 19, 2026), follow the remediation order printed by --check-host: stop the persistence service first, delete its files, then rotate credentials. Revoking a monitored token before stopping the service is designed to trigger a destructive wipe.
For per-wave source advisories with IoC enumerations, see the ### Security section of each release entry in CHANGELOG.md. The original Shai-Hulud disclosures live at:
When a new wave lands, check these first. Primary research teams publish the per-package version lists, file hashes, and C2 IoCs we add to this repo — always source additions from one of these and cite it in the PR. News aggregators are where a wave usually surfaces first; use them to learn a wave exists, then pull the actual IoCs from a primary source.
Primary threat-research teams (IoC enumerations):
News aggregators (early awareness):
MIT — see LICENSE.
1,396 followers · starred Nov 2025
113 followers · starred Nov 2025
65 followers · starred Nov 2025
509 followers · starred Nov 2025
A simple project to detect the Shai-Hulud npm supply chain attack
Shell
410
268 commits
updated Oct 12, 2026
A Bash script that scans a project — or many projects at once — for known traces of the September 2025 → October 2026 npm, PyPI, Composer, Crates, Go, Hex, and RubyGems supply-chain attacks. Cross-checks 5,791 confirmed bad package versions and a library of content-pattern IoCs (file hashes, C2 domains, dead-man's-switch artifacts, wipe-threat strings, AI-assistant config droppers, etc.).
git clone https://github.com/Cobenian/shai-hulud-detect
cd shai-hulud-detect
chmod +x shai-hulud-detector.sh
# Scan one project
./shai-hulud-detector.sh /path/to/your/project
# Scan every project under one or more parent dirs and get one aggregate report
./shai-hulud-detector.sh --bulk ~/dev ~/work
# Save findings to a file (same format as CI-friendly logs)
./shai-hulud-detector.sh --save-log report.log /path/to/project
Exit codes (drop straight into CI): 0 clean · 1 high-risk · 2 medium-risk · 3 per-project scan errored (bulk mode only).
The detector looks for two kinds of evidence on disk:
package.json, lockfile, pyproject.toml, requirements.txt, Pipfile, poetry.lock, uv.lock, etc. is parsed and the resolved versions are checked against the 5,791-entry list in compromised-packages.txt. Transitive deps inside node_modules/ are checked too, not skipped.| Wave | Date | Scope |
|---|---|---|
| Chalk/Debug crypto theft | 2025-09-08 | 18+ packages, ~2B weekly downloads |
| Shai-Hulud worm | 2025-09-14 | 517+ packages (@ctrl, @crowdstrike, …) |
| Shai-Hulud "Second Coming" (fake Bun) | 2025-11-24 | 1,100+ packages |
| Golden Path variant | 2025-12-28 | renamed Bun-attack files |
| SANDWORM_MODE workflow poisoning | 2026-02-17 | 19 packages + GitHub Action |
| Axios RAT compromise | 2026-03-31 | [email protected]/0.30.4 + plain-crypto-js |
| Mini Shai-Hulud / TanStack | 2026-05-11 | 400+ versions, dead-man's-switch |
| Megalodon (GitHub-repo backdooring) | 2026-05-18 | 5,561 repos via stolen PATs; @tiledesk/[email protected] as npm fallout |
| Mini Shai-Hulud / AntV (atool) | 2026-05-19 | 643 versions, 323 packages |
| Web3 / DeFi MCP-server typosquat | 2026-05-20 | 10 packages (chain-key-validator, defi-threat-scanner, …), exfiltrates SSH + wallet keys |
| Polymarket wallet drainer | 2026-05-21 | 9 packages from polymarketdev (polymarket-bot, polymarket-trader, …), fake wallet-onboarding prompt captures private keys |
| Bitwarden CLI ("Third Coming") | 2026-04-22 | @bitwarden/[email protected] via Checkmarx ast-github-action breach; bw1.js exfil to audit.checkmarx.cx |
| node-ipc backdoor | 2026-05-14 | [email protected]/9.2.3/12.0.1; IIFE in node-ipc.cjs, DNS exfil to sh.azurestaticprovider.net |
| Nx Console VS Code ext | 2026-05-18 | [email protected]; payload from orphan commit 558b09d7 in nrwl/nx, targets ~/.claude/settings.json (TeamPCP / GitHub breach) |
| TrapDoor (TeamPCP) | 2026-05-22→25 | 34 packages / 384+ versions across npm + PyPI + Crates; plants .cursorrules/CLAUDE.md AI-assistant droppers |
| Laravel-Lang tag-rewrite | 2026-05-22 | 700+ Composer tags force-rewritten (laravel-lang/lang, …); RCE on autoload, DebugElevator stealer |
| mouse5212 "Malware-Slop" | 2026-05-26 | mouse5212-super-formatter exfils Claude's /mnt/user-data via embedded GitHub PAT (unplowed3584) |
| art-template npm hijack | 2025-03 → 2026-05 | 4 versions ([email protected]), iOS browser exploit kit (UNC6691) |
| sl4x0 dependency confusion | 2025-06 → 2026-03 | 92+ packages across 32 *poc accounts, DNS exfil to oob.sl4x0.xyz (likely security research) |
durabletask PyPI worm | 2026-05-19 | pypi:durabletask:1.4.1-1.4.3, multi-cloud credential stealer + AWS SSM / k8s lateral movement |
| PyPI cross-spread (TeamPCP) | 2026-03 → 05 | litellm, telnyx, xinference, lightning, mistralai, guardrails-ai |
| Miasma "Phantom Gyp" worm | 2026-06-03 | 57 packages / 286 versions (@vapi-ai/server-sdk, ai-sdk-ollama, autotel-*, awaitly-*, executable-stories-*); novel binding.gyp command-substitution trigger bypasses preinstall-script monitors |
| Miasma "Hades" PyPI wave | 2026-06-07 | 19 PyPI packages / 37 versions (bramin, magique-ai, pantheon-agents, executor-engine, ufish); novel *-setup.pth Python startup-hook execs an _index.js loader on plain import, no install hook needed; C2 camouflaged under api.anthropic.com |
| IronWorm ("rustier cousin") | 2026-06-03 | 37 npm packages from the asteroiddao account (weavedb-*, arnext, cwao, wao, zkjson); Rust infostealer + eBPF rootkit via preinstall ELF hook, Exodus-wallet theft, leaked operator wallet 0x7e28…a4d6 |
| easy-day-js / Mastra AI | 2026-06-17 | 141 @mastra/* packages + mastra/create-mastra republished with an injected easy-day-js (dayjs typosquat) dependency; postinstall dropper disables TLS, pulls a cross-platform infostealer from C2 23.254.164.92/.123; North-Korea-attributed (Sapphire Sleet / BlueNoroff) |
| Miasma LeoPlatform / RStreams | 2026-06-25 | 23 npm packages (leo-*, rstreams-*, serverless-leo, …); Miasma worm reaches the AWS data-streaming ecosystem via the binding.gyp trigger + editor/CI auto-run hooks; new markers RevokeAndItGoesKaboom, thebeautifulmarchoftime |
| Shai-Hulud "Here We Go Again" (keyv / cacheable) — also tracked as ChainDrop (Microsoft, Elastic) | 2026-08-04 | Largest wave by install volume: 444 npm packages / 2,236 versions across 15 scopes, plus 3 Go modules / 82 versions, >2B monthly installs (keyv, flat-cache, file-entry-cache, cacheable-request, cacheable, cache-manager, @servicetitan/*, @ornikar/*, @onereach/*, @or-sdk/*, @qlik/*, @nebula.js/*, @umacloud/*); "preinstall": "node setup.mjs" bootstraps Bun 1.3.13 → 727KB Math_Symbol.js; primary exfil is to GitHub dead-drop repos described Shai-Hulud: Here We Go Again, with no hard-coded C2 — the payload reads its live domain from Ethereum contract 0xE1f2395e… at run time, so hosts rotate without republishing (npm-cache[.]com → awqhnjewqjkl[.]icu on Aug 4); poisoned releases carry valid npm OIDC provenance / SLSA attestation |
| Mini Shai-Hulud resurfacing | 2026-09-07 | 4 npm packages / 4 versions (blueai-cli, bmc-i18n-extract-cli, bmc-translate-utils, feishu-docx-mcp), all published by one account within an hour; the byte-identical May 19 @antv payload (index.js, hash in MALICIOUS_HASHLIST) via "preinstall": "bun run index.js", 111 days after it was last seen |
| "supplychain.local" Go-binary worm | 2026-09-23 | A new family, not Shai-Hulud code: @memtensor/[email protected]/0.1.23/0.1.25 (the even versions in between are clean maintainer republishes) and pypi:[email protected]. A multiplatform Go sckit binary (6 hashes in MALICIOUS_HASHLIST) steals tokens and republishes through npm, PyPI and a planted GitHub Actions workflow. It runs on invocation, not at install |
| tensorlake (ChainDrop family) | 2026-10-08 | [email protected], published by the project's own release workflow with provenance after an attacker commit to main; "preinstall": "node lib/setup.mjs" → Bun → lib/Math_Symbol.js (both hashed). Same payload family as the keyv wave but new infrastructure: C2 iseekaigogo[.]com, rotation contract 0xb614155F…, operator wallet 0x779f83aE…, and the WORMTAG infection-tag variable, all matched |
For per-wave IoC inventories, payload hashes, source advisories, and version-by-version lists, see CHANGELOG.md.
⚠️ Dead-man's-switch warning
Two waves (May 11 and May 19, 2026) install a persistence daemon (
gh-token-monitor/kitty-monitor) that wipes the host if its monitored GitHub token is revoked. Run with--check-hostto detect the persistence artifacts. If they're present, stop and remove the service BEFORE rotating any tokens. The console summary and aggregate report both print a safe remediation order.
| Ecosystem | Auto-detected via | Status |
|---|---|---|
| npm | package.json, package-lock.json, yarn.lock, pnpm-lock.yaml | Full support |
| PyPI | pyproject.toml, requirements*.txt, Pipfile, Pipfile.lock, poetry.lock, uv.lock, setup.py, setup.cfg | Full support (pure-bash awk parsers, no Python required) |
| Composer | composer.json, composer.lock | Full support (PHP / Packagist; added for the Laravel-Lang wave) |
| Crates | Cargo.toml, Cargo.lock | Full support (Rust / crates.io; added for the TrapDoor wave) |
| Go | go.mod, go.sum | Full support (Go modules; added for the Miasma LeoPlatform wave. Versions keep their canonical leading v) |
| Hex | mix.exs, mix.lock | Full support (Elixir / Hex.pm; mix.lock is authoritative, mix.exs requirements are best-effort) |
| Gem | Gemfile, Gemfile.lock | Full support (RubyGems / Bundler; Gemfile.lock specs are authoritative, Gemfile requirements are best-effort) |
Auto-detection looks for marker files in your tree, skipping node_modules/, vendor/, .venv/, venv/, .tox/, site-packages/, dist/, build/, and similar trees. That exclusion only decides which checks to run — content inside node_modules/ is still fully scanned for compromised versions and malware indicators. Override auto-detection with --ecosystem=npm, --ecosystem=pypi, --ecosystem=all, or a comma-separated list.
- name: Shai-Hulud scan
run: |
chmod +x ./shai-hulud-detector.sh
./shai-hulud-detector.sh --save-log shai-hulud-report.log .
# Job fails on exit 1 (high-risk) or 2 (medium-risk)
- uses: actions/upload-artifact@v4
if: always()
with:
name: shai-hulud-report
path: shai-hulud-report.log
For GitLab CI, Jenkins, and a custom exit-code handler in shell, see docs/ci-examples.md — or just call the script and switch on $?, the exit-code contract is the same everywhere.
--bulk)./shai-hulud-detector.sh --bulk ~/dev ~/work # auto-discover all projects
./shai-hulud-detector.sh --bulk --paranoid --bulk-output ./audit ~/dev
./shai-hulud-detector.sh --bulk --bulk-list ~/dev # dry-run: print what would be scanned
Project discovery is content-aware: a directory with .git, a package.json, a pyproject.toml, Cargo.toml, go.mod, etc. is one scan unit (monorepos stay whole). "Bucket" folders like ~/dev/clients/<client>/<project> are descended into. node_modules, vendor, dist, build dirs, and hidden dirs are never entered. The detector's own repo is skipped automatically. Unreadable directories are reported instead of silently skipped.
Output goes to ./shai-hulud-bulk-report-<timestamp>/ (or --bulk-output DIR):
shai-hulud-bulk-report-<timestamp>/
├── aggregate-report.md # summary tables + per-project results + remediation
└── per-repo/
├── <project>.findings.log # severity-grouped file paths
└── <project>.console.txt # full scan output, ANSI-stripped
--paranoid)Adds typosquatting detection and network-exfiltration heuristics on top of the core checks. These are general-purpose security signals, not Shai-Hulud-specific, and produce more false positives — useful for audits, not recommended for CI gating.
--check-host)Walks $HOME for the May 2026 dead-man's-switch artifacts (gh-token-monitor, kitty-monitor). Off by default. See the warning above before remediating any findings.
--save-log FILE)Writes flagged file paths grouped by severity, in a format friendly to grep and CI artifact uploads:
# HIGH
/path/to/router_init.js
/path/to/package.json
# MEDIUM
/path/to/suspicious-content.js
# LOW
/path/to/namespace-warning.json
--json FILE)For CI gates and tooling that consume findings as data (rather than parsing console text), --json writes a structured report. It uses the same severity tiers as --save-log but keeps the reason for each finding (which --save-log drops):
./shai-hulud-detector.sh --json report.json /path/to/project
{
"schema_version": "1.0",
"tool": "shai-hulud-detector",
"tool_version": "3.9.0",
"generated_at": "2026-06-26T00:00:00Z",
"scan_path": "/path/to/project",
"summary": { "high": 7, "medium": 0, "low": 0 },
"risk_level": "high",
"findings": [
{ "severity": "HIGH", "file": "package.json", "line": 6, "message": "[email protected]" }
]
}
line is a best-effort pointer at the dependency line in the manifest for package-shaped findings (name@version, @scope/name@version); it is null for prose findings or when no match is found. The exit-code contract (0/1/2) is unchanged and remains the authoritative CI signal — --json and --save-log can be combined.
--jsonis the only mode that requiresjq; it fails fast with a clear message ifjqis missing. The default text output keeps the tool's zero-runtime-dependency guarantee.
| Flag | Effect |
|---|---|
--json FILE | Write findings as structured JSON (severity/file/line/message + summary). Requires jq. |
--check-semver-ranges | Flag ^/~ ranges that could resolve to compromised versions (informational, LOW risk). |
--ecosystem LIST | Restrict checks to npm, pypi, all, or a comma-separated list. Default: auto-detect. |
--parallelism N | Threads for parallelisable steps. Defaults to your CPU count. |
--use-git-grep / --use-ripgrep / --use-grep | Force a specific grep tool. Default: auto-select fastest available. |
--bulk-depth N | Depth cap for bulk discovery (default 3). --bulk-depth 1 = flat. |
--bulk-list | With --bulk: print what would be scanned and exit. |
--bulk-output DIR | Where to write the bulk report (default ./shai-hulud-bulk-report-<timestamp>/). |
find pass, categorized by extension).compromised-packages.txt via a sorted set-intersection (comm -12).bundle.js, setup_bun.js, router_init.js, tanstack_runner.js, cat.py, node-ipc.cjs, etc.) and compare against 20 known-malicious SHA-256s.optionalDependencies patterns.$HOME for persistence artifacts (--check-host); run typosquatting + network-exfil heuristics (--paranoid); flag latent semver-range risk (--check-semver-ranges).Detection is read-only. The script never modifies, deletes, or quarantines anything — manual review and remediation are on you.
✅ No indicators of Shai-Hulud compromise detected. ← clean, exit 0
🚨 HIGH RISK: ... ← exit 1, immediate action
⚠️ MEDIUM RISK: ... ← exit 2, manual review
HIGH: definitive indicators — compromised package version, known-malicious file hash, malicious workflow, dead-man's-switch artifact. Stop and remediate.
MEDIUM: suspicious patterns that warrant a look — semver range that could match a compromised version, references to webhook redirector domains, suspicious git branches.
LOW: informational notes — namespace warnings (packages from affected namespaces at safe versions), legitimate-looking patterns that share shape with attack techniques.
mapfile). macOS ships 3.x; install with brew install bash and invoke via /opt/homebrew/bin/bash.find, grep, awk, sed, sort, comm, shasum (or sha256sum), xargs. All POSIX-portable.The script auto-selects the fastest available grep tool (git grep > ripgrep > grep). No runtime dependencies on Python, Node, or anything else.
compromised-packages.txt — new variants need a list update.compromised-packages.txt is a flat text file:
# Comments start with #
axios:1.14.1 # bare entry = npm (back-compat)
npm:@tanstack/react-router:1.169.5 # explicit npm prefix
pypi:mistralai:2.4.6 # PyPI entry
composer:laravel-lang/lang:15.29.5 # Composer / Packagist entry
crates:sui-move-build-helper:0.1.0 # Crates.io / Cargo entry
go:github.com/verana-labs/verana-blockchain:v0.10.1-dev.20 # Go module (version keeps its leading "v")
hex:some_pkg:1.2.3 # Elixir / Hex.pm entry
gem:some_gem:1.2.3 # RubyGems entry
For campaigns where every version of a package is malicious (e.g. TrapDoor, Laravel-Lang's tag rewrite), per-version entries can't keep up — detection is done version-agnostically by a dedicated check_*_indicators function that name-matches the dependency in any manifest.
To add new packages from a fresh advisory: append entries in that format, run ./run-tests.sh, open a PR. Source the additions from a reputable security firm (Socket, StepSecurity, Aikido, Snyk, JFrog, Wiz, Semgrep, SafeDep, GitGuardian, OX Security) and cite them.
./run-tests.sh # full suite, 296 checks
./shai-hulud-detector.sh test-cases/<fixture-name> # run one fixture manually
Each subdirectory of test-cases/ is a self-contained fixture (clean projects, infected projects with the various attack signatures, ecosystem-specific tests, paired clean/dirty pairs). Run ls test-cases/ to see them all.
PRs welcome — especially new compromised-package entries as fresh waves are disclosed.
compromised-packages.txt, add a fixture under test-cases/, register it in run-tests.sh.check_* function in shai-hulud-detector.sh, add an assertion in run-tests.sh../run-tests.sh must pass.Don't include actual malware in test fixtures — inert string constants and synthetic files only.
Detection only. The script does not remove malicious code, downgrade packages, or block installs. Verify findings, then remediate manually.
For the dead-man's-switch waves (May 11 + May 19, 2026), follow the remediation order printed by --check-host: stop the persistence service first, delete its files, then rotate credentials. Revoking a monitored token before stopping the service is designed to trigger a destructive wipe.
For per-wave source advisories with IoC enumerations, see the ### Security section of each release entry in CHANGELOG.md. The original Shai-Hulud disclosures live at:
When a new wave lands, check these first. Primary research teams publish the per-package version lists, file hashes, and C2 IoCs we add to this repo — always source additions from one of these and cite it in the PR. News aggregators are where a wave usually surfaces first; use them to learn a wave exists, then pull the actual IoCs from a primary source.
Primary threat-research teams (IoC enumerations):
News aggregators (early awareness):
MIT — see LICENSE.
1,396 followers · starred Nov 2025
113 followers · starred Nov 2025
65 followers · starred Nov 2025
509 followers · starred Nov 2025