← Back to Blogs
HN Story

Mini Shai-Hulud: Anatomy of a Massive npm Supply Chain Attack

May 21, 2026

Mini Shai-Hulud: Anatomy of a Massive npm Supply Chain Attack

The npm ecosystem has once again fallen victim to a large-scale supply chain compromise. On May 19, 2026, the npm account atool was compromised, leading to the publication of 637 malicious versions across 317 different packages in a rapid, 22-minute automated burst.

This wasn't a simple credential stealer. The attack deployed a sophisticated toolkit known as "Mini Shai-Hulud," a direct evolution of the framework used in a high-profile SAP compromise just weeks prior. With affected packages like size-sensor and echarts-for-react seeing millions of monthly downloads, the blast radius was immense, targeting everything from AWS infrastructure to the emerging landscape of AI coding agents.

The Infection Vector: Beyond the Preinstall Hook

The attacker utilized two primary execution paths to ensure the payload reached the target system:

  1. The preinstall Hook: Every compromised version added a preinstall script (bun run index.js). Because npm resolves semver ranges (e.g., ^3.0.6) to the highest available version, projects performing a clean install automatically pulled the malicious updates regardless of the latest tag.
  2. GitHub Imposter Commits: In a more insidious move, 630 versions injected an optionalDependencies entry pointing to the antvis/G2 repository. The attacker used "orphan commits"—commits pushed to a fork that are fetchable by SHA via the parent repository's namespace. By forging the authorship to look like a legitimate maintainer, the attacker hosted a second copy of the payload on GitHub without needing write access to the target repo.

Technical Deep Dive: The Mini Shai-Hulud Toolkit

Credential Harvesting and Cloud Probing

The payload is a 498KB obfuscated Bun script designed for maximum extraction. It employs a scanner architecture that targets over 80 environment variables and uses regex patterns to identify:

  • Cloud Keys: AWS (including EC2 IMDSv2 and ECS metadata), GCP service accounts, and Azure credentials.
  • Tokens: GitHub PATs, npm tokens, Slack tokens, and HashiCorp Vault tokens.
  • Infrastructure: Kubernetes service account tokens and SSH keys.

Container Escape and Privilege Escalation

If the malware detects a Docker socket (e.g., /var/run/docker.sock), it attempts to escape the container. It constructs a privileged Docker container with host bind mounts, effectively granting the attacker full access to the host filesystem.

AI Agent Hijacking

One of the most novel aspects of this attack is the targeting of AI coding tools. The payload injects SessionStart hooks into .claude/settings.json (for Claude Code) and .vscode/tasks.json (for Codex/VS Code).

Whenever a developer starts an AI session or opens a project folder, a Bun bootstrapper is triggered, re-executing the malware. This ensures that even if the initial npm infection is cleaned, the malware persists through the developer's AI-assisted workflow.

C2 Infrastructure: GitHub as a Dead-Drop

Rather than connecting to a suspicious external server, the attacker used the GitHub API as a Command and Control (C2) channel, making the traffic blend in with normal developer activity.

  • Exfiltration: Stolen data was committed as Git objects to public repositories created under the compromised token. These repos followed a Dune-themed naming pattern (e.g., fremen-sandworm-315) with the description "Shai-Hulud: Here We Go Again."
  • The kitty-monitor Backdoor: The payload installs a persistent systemd service or macOS LaunchAgent called kitty-monitor. This Python daemon polls the GitHub Search API hourly for commits containing the keyword firedalazer. If it finds a commit with a valid RSA-PSS signature, it downloads and executes arbitrary Python code from the signed URL.

CI/CD Sabotage and Sigstore Abuse

In CI environments, the attack becomes even more dangerous. The payload exchanges GitHub Actions OIDC tokens for npm publish tokens, allowing the attacker to publish new packages using the pipeline's own identity.

Furthermore, the attacker abused Sigstore (Fulcio + Rekor) to sign artifacts. By using stolen OIDC tokens, they created legitimately signed artifacts with forged provenance, potentially tricking downstream security scanners that rely on Sigstore for trust verification.

Community Reaction and Defensive Strategies

The scale of this attack has sparked significant debate within the developer community regarding the inherent insecurity of the npm trust model.

The "Cat and Mouse" Game

Many developers expressed frustration with the reliance on lifecycle scripts (preinstall/postinstall). As one commenter noted:

"Lifecycle scripts should be disabled by default in NPM. It's a convenience feature that provides built-in Arbitrary Code Execution as a feature... and every one of these widespread NPM worm style attacks has propagated through it."

Recommended Mitigations

To protect against similar supply chain attacks, security researchers and community members suggest the following:

  • Dependency Cooldowns: Use settings like npm config set min-release-age=2 to avoid installing packages published within the last few days, which is when most malicious bursts are detected and removed.
  • Hardened Environments: Run development workloads in rootless VMs or DevContainers (e.g., using Podman instead of Docker) to mitigate container escape attempts.
  • Lockfile Auditing: Use tools like pmg (Package Manager Guard) or vet to analyze lockfiles for unexpected size spikes or new lifecycle scripts.
  • Vendor Dependencies: For critical projects, consider vendoring dependencies—cloning them into your own repo and freezing them—to eliminate the risk of upstream compromises.

Summary of Indicators of Compromise (IoC)

Category Indicator
Payload SHA256 a68dd1e6a6e35ec3771e1f94fe796f55dfe65a2b94560516ff4ac189390dfa1c
C2 Keyword firedalazer in GitHub commit messages
Persistence kitty-monitor.service or com.user.kitty-monitor.plist
Files ~/.local/share/kitty/cat.py, .claude/setup.mjs, .vscode/setup.mjs
Repo Pattern {DuneWord1}-{DuneWord2}-{0-999} (e.g., mentat-melange-123)

References

HN Stories