01 — The incident
A preinstall hook did all the work
Attackers took over the keyv maintainer's GitHub account and cut releases with valid GitHub Actions signatures — the packages passed every normal supply-chain check. The campaign brands itself “Shai-Hulud: Here We Go Again”.
setup.mjs, Math_Symbol.js) plus "preinstall": "node setup.mjs" — the payload runs the moment npm install touches the package..env files and SSH keys via ~200 filesystem patterns.npm-cache[.]com..vscode/tasks.json and .claude/settings.json so your editor or AI agent re-runs it later.02 — Am I affected
You depend on these. The question is which version.
These are transitive dependencies of half the npm ecosystem — keyv alone sees ~604M monthly downloads, and the eleven first-wave packages combine to over 2 billion. Malicious only in the exact versions below, published August 4 and since pulled. If you — or your CI, or a coding agent — ran npm install in that window, spend ten minutes and scan.
| Package | Bad version | Package | Bad version |
|---|---|---|---|
| keyv | 6.0.0 | cache-manager | 7.2.10 |
| flat-cache | 6.1.24 | @cacheable/node-cache | 3.1.2 |
| file-entry-cache | 11.1.6 | @cacheable/utils | 2.5.1 |
| cacheable-request | 13.0.20 | @cacheable/net | 2.1.1 |
| cacheable | 2.5.1 | ecto | 5.0.1 |
| @cacheable/memory | 2.2.1 | + payload filenames carried over from the Nov 2025 wave | |
03 — Run the scan
Download one file. Read it. Run it.
Each script is a single self-contained file — no install, no dependencies, no companion files. It only reads your disk: nothing is modified, deleted, or quarantined. Open it on GitHub first if you like; they're short on purpose.
Windows · PowerShell
irm https://raw.githubusercontent.com/apsolut-public/scripts/main/security/scan-npm-shai-hulud.ps1 -OutFile scan.ps1
.\scan.ps1 -Path D:\sites
Linux / macOS / WSL · bash
curl -fsSLO https://raw.githubusercontent.com/apsolut-public/scripts/main/security/scan-npm-shai-hulud.sh
bash scan-npm-shai-hulud.sh ~/sites
Both versions run the same checks, print the same counts, and return the same exit codes — either one drops straight into CI or a pre-commit hook. -Quiet / --quiet prints findings only. -Deep / --deep extends the content sweep into node_modules — off by default because it dominates runtime on vendored trees while other checks already cover packages by filename and version.
04 — The six checks
Filename, hook, version, workflow, persistence, strings
One full directory walk total — about 5–6 min on a 7.5-million-file tree — then every check runs against that list. All read-only.
setup.mjs, math_init.js, Math_Symbol.js, plus the Nov 2025 bun payloads — SHA-256 printed with every hit.package.json whose preinstall launches a payload.node_modules — scoped names included, nested trees resolved to the right package.toJSON(secrets))..vscode/tasks.json or .claude/settings.json.Known false positives — and why they're reported anyway
regenerate-unicode-properties, a very common transitive dependency, legitimately ships a file called Math_Symbol.js full of unicode data. Those hits are reported as ignored: with a count instead of findings — unless the hash matches a known-bad sample. A real payload planted at that path is still caught.
Scanning a tree that contains the scripts repo itself will flag the scripts and their README under check 6 — they carry the indicator strings by definition. Each script skips its own file, but not its sibling.
05 — If it finds something
Assume the tokens are burned
A hit with a non-matching hash means an unknown variant, not a clean file. Triage every finding by hand — then act in this order, because credential theft is the whole point of the malware.
| Step | Action | Why first |
|---|---|---|
| 1 | Rotate credentials — npm tokens, GitHub PATs/OAuth/Actions tokens, cloud keys, anything that lived in a .env on that machine. | Stealing these was the payload's only job; everything else is cleanup. |
| 2 | Audit your GitHub account — repositories you didn't create, workflow files you didn't write. | The campaign exfiltrates through victim-owned repos. |
| 3 | Reinstall clean — delete node_modules, pin or override to safe versions, reinstall from the lockfile. | Removes the payload the scanner found. |
| 4 | Check persistence — .vscode/tasks.json and .claude/settings.json for entries you didn't put there. | Otherwise your editor or agent re-infects the clean tree. |
| 5 | Follow Aikido's remediation in the write-up — they also offer a free repo scan and install-time blocking via their tooling. | They found the campaign; their guidance tracks it as it evolves. |
npm audit, or Aikido. The indicator lists live in one marked block at the top of each script; when new IoCs are published, that block is the only thing that changes.
06 — Questions
Asked and answered
Is it safe to run a random security script from the internet?
Don't take that on faith — that skepticism is exactly what this incident rewards. Each script is one file, short by design, with every indicator in a marked block at the top. Open it, read it, then run it. It only reads your disk and writes nothing but console output and a temp file list.
npm already pulled the bad versions — why scan at all?
Pulling stops new installs. If a poisoned version landed on your machine during the window, the payload already ran: your tokens may be gone and persistence may be planted. The scan answers "did it touch me", which the registry can't.
The scan flagged a file but the hash doesn't match — false positive?
Not automatically. A matching hash confirms the exact published sample; a non-matching hash at a payload path means an unknown variant until proven otherwise. Known-benign collisions (like regenerate-unicode-properties) are already filtered to ignored:. Triage by hand and rotate tokens before writing anything off.
What's the single next step?
Copy the two-line command for your platform from §03 and run it against your projects root. Ten minutes, read-only, and exit code 0 lets you stop thinking about this.
07 — Source
Two files, nothing else
| File | Platform | Status |
|---|---|---|
scan-npm-shai-hulud.ps1 | Windows · PowerShell 7+ | maintained |
scan-npm-shai-hulud.sh | Linux / macOS / WSL · bash | maintained |
Part of apsolut-public/scripts — standalone developer scripts: every one a single file you can download or copy-paste and run, no install, no shared library.