637 npm Packages Compromised in 39 Minutes. The Malware Installs a Claude Code SessionStart Hook.

The Shai-Hulud worm stole npm tokens and republished packages autonomously. One of its persistence mechanisms: a Claude Code hook in your .claude/settings.json.

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’s behavioral audit. 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.