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. By then, the packages had been live for 39 minutes.
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, posting to t.m-kosche.com:443/api/public/otel/v1/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 have 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. The non-AntV packages 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 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 scores change.
Top comments (0)