On May 19, 2026, between 01:39 and 02:18 UTC, a single compromised npm account published 639 malicious package versions across 323 packages. The entire attack took under 40 minutes.
The packages included jest-canvas-mock (2.2M weekly downloads), echarts-for-react (1.1M), size-sensor (1.2M), timeago.js (243K), and most of the @antv visualization suite. Total blast radius: roughly 16 million weekly downloads.
This wasn't a human typing npm publish 639 times. This was a worm.
How the self-propagation works
The atool npm account was compromised (how is still unknown). That account had publish access to hundreds of packages. The initial payload did what you'd expect — harvested credentials from 80+ environment variables and 100+ file paths across AWS, GCP, Azure, GitHub, Kubernetes, and database systems.
Then it did something different: it searched for npm tokens with the bypass_2fa scope. In GitHub Actions environments, the malware exchanged OIDC tokens for per-package npm publish credentials. It then republished additional packages with itself embedded. An npm worm.
Two waves hit the registry. First: ~317 versions at 01:39. Second: ~314 versions 26 minutes later at 02:05. Detection started around 02:18. By then, the packages had been live long enough.
The persistence mechanisms
The exfiltrated credentials are serialized as JSON, gzip-compressed, encrypted with AES-256-GCM, and wrapped with RSA-OAEP. The exfiltration channel disguises traffic as OpenTelemetry traces.
A backup channel creates public repos under the victim's GitHub account and commits encrypted credential dumps with Dune-themed naming patterns.
Here's where it gets personal if you use Claude Code or VS Code:
The malware installs a SessionStart hook in .claude/settings.json. It also drops VS Code task automation in .vscode/tasks.json and a background daemon that polls GitHub every 60 seconds for RSA-signed commands.
And there's a dead man's switch. If the stolen GitHub token gets revoked, the malware runs rm -rf ~/.
These aren't hypothetical persistence vectors. They're documented by Akamai, CSA, and Expel.
What the packages looked like before the attack
I scored the compromised packages using Commit. The non-AntV packages tell the clearest story:
| Package | Score | Publishers | Downloads/wk | Risk |
|---|---|---|---|---|
| canvas-nest.js | 50 | 1 | 650 | WARN: no release 12+ months |
| timeago.js | 65 | 2 | 243K | WARN: no release 12+ months |
| size-sensor | 66 | 1 | 1.2M | HIGH: sole publisher + >1M/wk |
| echarts-for-react | 71 | 1 | 1.1M | HIGH: sole publisher + >1M/wk |
| jest-canvas-mock | 72 | 2 | 2.2M | WARN: no release 12+ months |
Three of these five had a sole npm publisher. Two are stale — no release in over a year, still pulled by millions of projects weekly. That's exactly the profile that makes account takeover both easy and high-impact.
The @antv packages scored higher (84–89) because they have 17–18 maintainers. But that's exactly how the attack worked: atool was one of those 18 maintainers. More publishers means more attack surface when any one of them can push.
Protect your editor
If you use Claude Code, Cursor, or Windsurf, you can gate package installs before they run:
npx proof-of-commitment hook
This installs a pre-install check that intercepts npm install, pip install, and cargo add. CRITICAL packages (sole publisher + millions of downloads — the exact Shai-Hulud profile) are blocked before they execute. The hook writes .claude/settings.json, .cursor/hooks.json, and .windsurf/hooks.json so the gate works regardless of which editor is driving.
The irony: the same file the worm writes to for persistence (.claude/settings.json) is the one you use to defend against it.
What to check
If your package-lock.json or yarn.lock includes any of these packages, check which versions you installed between 01:39 and 02:18 UTC on May 19.
Then check the rest of your dependency tree:
npx proof-of-commitment --file package-lock.json
The packages that scored 50-72 before this attack (sole publishers, stale releases, high downloads) are the same profile that got compromised in the LiteLLM attack, the axios attack, and now this one.
The pattern doesn't change. The entry point is always the same: one compromised account with publish access to a widely-installed package.
What's different about this one
Previous supply chain attacks hit one package at a time. This one propagated. It turned compromised npm tokens into more compromised packages. The window between first publish and detection is getting shorter, but the blast radius is getting wider.
And the persistence mechanisms are evolving. Targeting .claude/settings.json and .vscode/tasks.json means the malware survives container restarts and embeds itself in developer tooling. The exact environment where you decide which packages to trust.
Run a supply chain audit on your project — or set up monitoring to get alerted when a package in your tree degrades.
Top comments (0)