Skip to main content
Back to Blog
automationsupply-chain-attacknpm-securitydevsecopsbun-runtimegithub-actionsedr-evasionapplication-security

When the Worm Brings Its Own Runtime: What 'Mini Shai-Hulud' Tells Us About the Next Wave of npm Attacks

Four SAP npm packages were backdoored in a supply chain attack that used a Bun runtime to evade EDR detection. See the CI approval gap and how to fix it.

Zyfolks Team ·

The most interesting line in the SAP supply chain incident isn’t that four npm packages got backdoored on April 29, 2026 — it’s that the malware shipped its own JavaScript runtime to dodge whatever was already installed on the box. The attackers who hit mbt, @cap-js/db-service, @cap-js/sqlite, and @cap-js/postgres didn’t trust Node to run their payload. They used preinstall to download Bun 1.3.13 from GitHub Releases and ran an 11,678,349-byte obfuscated stealer through it. That’s not a tactical detail. That’s a preview of how the next year of supply chain attacks is going to look.

How a Missing Approval Gate Took Down Four SAP Packages

According to SAP engineer patricebender’s root-cause statement filed in cap-js/cds-dbs PR #1592, the entry vector was structural: “the workflow had publish permissions without any manual approval gate.” An unauthorized actor pushed malicious commits, the GitHub Actions release workflow picked them up, and the cloudmtabot npm account published mbt@1.2.48, @cap-js/sqlite@2.2.2, @cap-js/postgres@2.2.2, and @cap-js/db-service@2.10.1 between 09:55 and 12:14 UTC.

Why this matters: every team that ships from CI is one push-to-protected-branch away from the same outcome. The GitHub Actions OIDC trusted-publisher flow eliminates long-lived npm tokens, which is good. But it does not eliminate the publish capability of any commit that lands in main — which is exactly what the attackers exploited. SAP’s clean post-incident releases at 13:46 UTC (@cap-js/db-service@2.11.0, @cap-js/sqlite@2.4.0, @cap-js/postgres@2.3.0) shipped through a different account, cap-npm, gated behind an environment review. The fix was a configuration change.

If you’re a team running automated npm publish from main without a manual approval environment, you have the same vulnerability SAP just patched. Add the gate this week.

Prediction: GitHub will eventually default new “publish to npm” workflow templates to require a protected environment, the same way Vercel and Cloudflare default production deploys to require approval. The current default is too easy to misconfigure.

Why Bun-as-Payload-Runtime Is the Detail That Matters Most

The compromise pattern across all four packages is identical: a 4.5 KB setup.mjs plaintext loader (byte-identical across packages) and an execution.js blob at exactly 11,678,349 bytes. The preinstall hook checks whether bun is on PATH; if not, it pulls Bun 1.3.13 from GitHub Releases and runs bun execution.js.

Why this matters: most npm-based malware in the wild is Node-flavored JavaScript executing under whatever Node version is on the build agent. EDR tooling, Datadog runner instrumentation, and CI-side monitoring are tuned for that. A standalone Bun binary running an 11.6 MB blob from a temp directory is not what those agents watch. The payload also calls Bun-native APIs — Bun.gunzipSync(), Bun.main — meaning even a static analyzer that understands Node will partially miss the execution graph.

Practical example: if you’re a security engineer who has spent the last year writing detections for node processes spawned by npm install, this campaign sidesteps every one of them. Your detection now needs to fire on bun invocations from inside node_modules install hooks, which most teams have never had to consider.

Editorial take: the attackers picked Bun because it ships as a single static binary with batteries included. That same property is why teams adopted Bun in the first place — and why the next three campaigns will probably copy this pattern.

The AI Agent Config Files Are the New Persistence Layer

According to StepSecurity’s static deobfuscation, the payload commits a .claude/settings.JSON to every GitHub repository the stolen token can write to. The file uses Claude Code’s SessionStart hook to re-execute the stealer whenever a developer opens a Claude Code session. It pairs that with a .vscode/tasks.JSON containing "runOn": "folderOpen", which fires when the project opens in VS Code. Both files are committed under claude@users.noreply.GitHub.com with the message "chore: update dependencies" — the kind of diff a tired reviewer waves through on a Tuesday.

Why this matters: the npm install side of supply chain attacks has been the focus of defensive tooling for a decade. Lifecycle hooks, postinstall scripts, dependency cooldowns — that’s all known surface. AI coding agent configuration files are not. They are checked into source control, they execute code on developer machines, and they are read by the agent without the same scrutiny we give to a shell script. The Microsoft VS Code team has already declared that tasks.JSON runOn: folderOpen is “By Design” because Workspace Trust is the mitigation. The Anthropic SessionStart hook issue was filed twelve days before this attack and remains open.

Practical example: imagine you maintain a popular open source library with a busy pull request queue. An attacker who has compromised one of your contributors’ npm tokens can quietly land a .claude/settings.JSON change disguised as a tooling update. Anyone on your team using Claude Code now executes attacker-controlled code on session start. The diff looks routine. The blast radius doesn’t.

Editorial take: AI agent config files are about to become the most-targeted persistence surface in source control, and the major agent vendors are not ready. Within twelve months, expect at least one major IDE or agent to adopt a default-deny posture on hooks defined inside repository config — closer to the trust model engineers reach for when they compare blockchain to traditional databases than the current “Workspace Trust covers it” stance.

The Encrypted Dead Drop Means You Have to Rotate Everything

Stolen credentials are committed to a public repository on the victim’s own GitHub account, tagged with the description “A Mini Shai-Hulud has Appeared.” The contents are AES-256-GCM ciphertext, with the AES key wrapped via RSA-OAEP-SHA256 against an attacker-controlled RSA-4096 public key embedded in the payload. Per direct GitHub API counts, 1,076 such repositories existed at 15:17 UTC on April 29.

Why this matters: defenders can see that something was stolen. They cannot see what. That asymmetry forces a worst-case rotation strategy. StepSecurity recovered 134 file-path patterns the payload reads — npm tokens, GitHub PATs, AWS/Azure/GCP credentials, Kubernetes kubeconfigs, SSH keys, password manager CLI tokens, .env* files, Docker configs, Terraform Cloud credentials, MCP server configs, crypto wallet keys for ten different chains, and ~/.claude.JSON. The payload also dumps Runner.Worker memory on Linux CI via /proc/{pid}/mem, so any GitHub Actions secret that touched the runner before the install step is in scope, even if it was never exposed to the install step itself.

Practical example: if you’re a team that ran any of the four affected versions during the roughly 10:00–14:00 UTC window — including transitively, since @cap-js/sqlite@2.2.2 declares @cap-js/db-service@^2.10.0 and pulls the malicious sibling on a clean install — the conservative play is full credential rotation across every endpoint reachable from the affected machine. Regulated industries take the worst of it — healthcare software stacks routing patient data through CI-built containers, where rotating an IAM role cleanly is rarely a one-click operation. There is no triage path that lets you skip rotation based on what’s in the dead drop.

Editorial take: the encrypted dead drop is going to become standard — cheap to implement, expensive to investigate around, asymmetric in the attacker’s favor. Expect every credible npm worm in 2026 to ship one.

FAQ

Q: What is Mini Shai-Hulud and how is it different from the original? A: Mini Shai-Hulud is the April 29, 2026 npm supply chain attack on four SAP-ecosystem packages, named after the dead-drop repository description string the payload uses. It reuses the Shai-Hulud branding from the September 2025 and November 2025 waves, and per StepSecurity’s deobfuscation it ships functional npm self-propagation code, but as of publication only the four originally compromised packages have been observed in the wild.

Q: Should I disable npm lifecycle scripts in CI? A: For most CI environments, yes — npm install --ignore-scripts neutralizes the entire preinstall-driven attack class that delivered the original Shai-Hulud, SHA1-Hulud, and this campaign. Allow scripts only for the specific packages that genuinely require them. pnpm v10 blocks lifecycle scripts by default, and Bun-as-installer ships with an explicit trusted-package allowlist; both improve on the npm CLI default.

Q: My team uses Claude Code. Are we exposed? A: Only if a compromised package ran on a machine that also had push access to a repository containing a .claude/settings.JSON file. The persistence vector requires the stolen GitHub token to write the config, and re-execution requires the developer to start a Claude Code session in that repo. Audit recent commits to .claude/settings.JSON and .vscode/tasks.JSON for unexpected additions, particularly under the claude@users.noreply.GitHub.com author identity with the commit message “chore: update dependencies.”

Key Takeaways

  • Add a manual approval gate to any GitHub Actions workflow that holds npm publish rights — SAP’s own root cause was the absence of one, and the same gap exists in most teams’ release pipelines.
  • Treat bun execution from inside node_modules install hooks as a high-signal detection event, because EDR rules tuned for Node-only payloads will miss this class of attack.
  • Review AI agent configuration files (.claude/settings.JSON, .vscode/tasks.JSON with runOn triggers) in PR diffs with the same scrutiny as package.JSON changes — they are now an active persistence target.
  • If a compromised version of mbt, @cap-js/db-service, @cap-js/sqlite, or @cap-js/postgres touched any machine in your build path, rotate every credential in scope; the encrypted dead drop means you cannot scope down based on what was actually stolen.
  • Audit transitive resolutions, not just direct dependencies — @cap-js/sqlite@2.2.2’s ^2.10.0 range on @cap-js/db-service pulled the malicious sibling on clean installs without anyone listing it directly.

Have a project in mind?

Tell us what you're building — we reply within 24 hours.