On May 19, 2026, between 01:39 and 02:18 UTC, a single compromised npm account published 637 malicious package versions across 323 packages. The entire attack took 39 minutes.
The packages included jest-canvas-mock (2.8M weekly downloads), echarts-for-react (1.1M), size-sensor (1.2M), timeago.js (295K), and most of the @antv visualization suite. Total blast radius: roughly 16 million weekly downloads.
This wasn't a human typing npm publish 637 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 547 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.
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 itself as OpenTelemetry traces, posting to t.m-kosche.com:443/api/public/otel/v1/traces.
A backup channel creates public repos under the victim's GitHub account, commits encrypted credential dumps with Dune-themed naming patterns.
Here's where it gets personal for anyone reading this on a machine with Claude Code installed:
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 at ~/.local/share/kitty/cat.py 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 ~/.
What the packages looked like before the attack
I scored the compromised packages using Commit, a behavioral supply chain scorer. The non-AntV packages — the ones most projects wouldn't think to audit — tell the clearest story:
| Package | Score | Publishers | Downloads/wk | Risk |
|---|---|---|---|---|
| canvas-nest.js | 50 | 1 | 1K | WARN: no release 12+ months |
| timeago.js | 65 | 2 | 295K | 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.8M | 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 account worked: atool was one of those 18 maintainers. More publishers means more attack surface when any one of them can publish.
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 39-minute 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 make decisions about which packages to trust.
Run a supply chain audit on your project — or set up monitoring to get alerted when scores change.
Top comments (0)