TL;DR
- Wiz Research posted a June 4, 9:20 UTC update on the Miasma npm worm: a new wave uses
binding.gyp, notpackage.jsonlifecycle scripts, to run code duringnpm install. - StepSecurity's writeup (published June 3) names the hook Phantom Gyp: 57 packages, 286+ malicious versions, first hit
@vapi-ai/server-sdkat 23:30 UTC June 3, then a rolling blast acrossjagreehalmaintainer families (ai-sdk-ollama,autotel,awaitly,executable-stories, and others). - The 157-byte
binding.gypmakes npm runnode-gyp rebuild; gyp command substitution runs a 4+ MB rootindex.jswhilepackage.jsonlists"main": "./dist/index.js"and declares no install scripts. - After install: download Bun, scrape GitHub Actions runner memory for masked secrets, harvest cloud tokens, forge SLSA/Sigstore provenance, republish infected tarballs, and drop
.claude/setup.mjs,.cursor/rules/setup.mdc, and similar files into repos the stolen token can write to.
Why this morning's advisory matters
Miasma is not a new name if you read supply-chain mail this week. Microsoft's June 2 post documents the June 1 @redhat-cloud-services/* wave: preinstall hooks, OIDC publishing from hijacked GitHub Actions, tarballs that still carried valid provenance. We covered the TanStack-shaped variant in May: poisoned CI, runner memory, trusted publishing.
Today's artifact is Wiz's June 4 changelog line: same payload family, different install surface. Security teams that only grep package.json for "preinstall" or "postinstall" will miss it entirely.
Phantom Gyp: node-gyp is the script
npm runs node-gyp rebuild whenever a published tarball ships a binding.gyp, even when the package is pure JavaScript and "main" points at ./dist/. StepSecurity recovered this file from executable-stories-demo@0.1.11 (SHA-256 ef641e956f91d501b748085996303c96a64d67f63bfeef0dda175e5aa19cca90 across versions):
{
"targets": [
{
"target_name": "Setup",
"type": "none",
"sources": ["<!(node index.js > /dev/null 2>&1 && echo stub.c)"]
}
]
}
The <!(...)> substitution runs node index.js during the gyp configure step. Output goes to /dev/null; && echo stub.c hands gyp a fake source name so the build does not error. Your lockfile scanners that flag lifecycle scripts never see a hook declared in package.json.
The tarball layout StepSecurity published is the tell:
binding.gyp: 157 bytes, malicious.- Root
index.js: ~4.5 MB obfuscated (not the package entry point). dist/index.js: ~27 KB, the real library, untouched.
Legitimate apps import from dist/; only install-time gyp touches the root blob.
What runs after gyp fires
StepSecurity ran @vapi-ai/server-sdk@1.2.2 under Harden-Runner audit mode and captured the process tree. Rough sequence: npm install → node-gyp rebuild → node index.js → curl Bun v1.3.13 from GitHub releases → bun run /tmp/p{random}.js → gh auth token → sudo python3 reading /proc/<Runner.Worker>/mem → shell pipeline hunting JSON shaped like GitHub Actions masked secrets → api.github.com uploads to repos under liuende501.
Decoded payload strings StepSecurity lists include AWS/GCP/Azure/Vault/Kubernetes collectors, npm bypass_2fa publish paths, and RubyGems extconf.rb injection templates. The worm can repack tarballs, request Fulcio/Rekor attestations, and push versions that look supply-chain-clean to provenance checkers.
Exfil repos on liuende501 use descriptions like "Miasma - The Spreading Blight" or a reversed "Shai-Hulud: Here We Go Again" string (StepSecurity ties that to their June 1 Red Hat writeup). Treat any machine that installed listed versions during the exposure window as compromised, not merely "possibly affected."
The part that poisons your editor, not just your registry token
This variant adds AI assistant config drops into repositories the malware can push to:
.claude/setup.mjs(Claude Code SessionStart hook).cursor/rules/setup.mdc.gemini/settings.json.vscode/tasks.jsonwithrunOn: folderOpen
StepSecurity says the injected files run under Bun, not Node, so process-tree monitors keyed on node miss them. The social-engineering line in-repo: "required for proper IDE integration and dependency setup." Once that file lands on main, every teammate who opens the project in an AI-assisted IDE inherits the hook.
That is a different failure mode from "we published a bad tarball once." It is persistent workspace poisoning.
What to do on your side
If you installed any version in StepSecurity's table during the exposure window, assume secrets on that host and in reachable CI are burned. Rotate npm tokens, GitHub PATs and Apps, cloud roles, Vault tokens, and anything else the runner or laptop could touch. Check for surprise repos, publishes, or workflow runs you did not author.
Hunt before the next install:
# binding.gyp with the Phantom Gyp stub pattern
find node_modules -name binding.gyp -exec grep -l 'stub.c' {} \;
# Root index.js far larger than dist/ (example threshold)
find node_modules -maxdepth 2 -path '*/index.js' -size +1M
# AI assistant backdoors
ls -la .claude/setup.mjs .cursor/rules/setup.mdc \
.gemini/settings.json .vscode/setup.mjs 2>/dev/null
CI jobs that do not need native builds: set ignore-scripts=true in the job .npmrc (npm docs). That blocks lifecycle scripts; it does not stop npm from invoking node-gyp when binding.gyp is present. For those packages you need lockfile review, install blocking on new publishes, or tooling that inspects tarball contents before install.
Registry-side: StepSecurity points at minimum release age gates and malicious-package DBs; npm's staged publishing (GA in CLI 11.15.0, GitHub changelog May 22) helps maintainers, not consumers who already trust a maintainer account.
Sources
- Wiz: Miasma supply chain attack (changelog update 2026-06-04 9:20 UTC on binding.gyp wave)
- StepSecurity: Miasma npm supply chain attack via Phantom Gyp (2026-06-03)
- Microsoft Security Blog: Preinstall to persistence, Red Hat npm Miasma (2026-06-02)
- Endor Labs: Trojanized ai-sdk-ollama / Miasma (2026-06-03)
- npm config: ignore-scripts
- GitHub changelog: staged publishing and install-time controls for npm (2026-05-22)