Node-gyp Supply Chain Compromise: A Self-Propagating npm Worm That Hides in binding.gyp
June 4, 2026
0 mins readA supply chain attack is actively spreading through the npm registry by abusing a file most security tooling never looks at: binding.gyp. Instead of relying on the well-monitored preinstall or postinstall lifecycle scripts, the malware ships a weaponized binding.gyp that triggers node-gyp to execute attacker-controlled code automatically during npm install. Snyk is tracking the incident as Node-gyp Supply Chain Compromise - June 2026, covering 57 affected packages across hundreds of malicious versions, all classified as Embedded Malicious Code at Critical severity.
The payload harvests developer and CI/CD credentials across npm, GitHub, AWS, GCP, Azure, HashiCorp Vault, and Kubernetes, exfiltrates them through attacker-controlled GitHub repositories, injects GitHub Actions workflows for persistence, and self-propagates by republishing packages from any maintainer account it can reach. StepSecurity, which first reported the campaign, named the install-time technique "Phantom Gyp" and tracks the wider campaign as "Miasma," a descendant of the Shai-Hulud worm family.
TL;DR
Attack type | Supply chain worm via compromised maintainer accounts |
Novel technique | Code execution at install time through |
Snyk tracking | |
Severity | Critical (Embedded Malicious Code) |
Incident date | June 3, 2026 (primary wave); earlier Miasma variant June 1, 2026 |
Packages compromised | 57 packages, hundreds of malicious versions |
Highest-traffic victims |
|
Malware behavior | Credential theft, GitHub Actions injection, persistence, worm propagation across npm and RubyGems |
Immediate action | Pin to known-good versions, run |
Affected packages
Snyk lists 57 affected packages. We confirmed the listed malicious versions are still resolvable on the public registry (for example, all four @vapi-ai/server-sdk versions return live tarballs from registry.npmjs.org), with publish timestamps on June 3 and 4, 2026. The highest-traffic victims, with weekly download figures from the npm registry API, are:
Package | Weekly downloads | Malicious versions |
|---|---|---|
| ~86,500 (api.npmjs.org) | 0.11.1, 0.11.2, 1.2.1, 1.2.2 |
| ~36,900 (api.npmjs.org) | 0.13.1, 1.1.1, 2.2.1, 3.8.5 |
| ~5,900 (api.npmjs.org) | 2.26.4, 3.4.3 |
| ~280 (api.npmjs.org) | 1.33.3 |
Most of the 57 packages cluster around a single npm account. Querying the registry shows all 25 autotel and autotel-* packages share the maintainer jagreehal, the same account that publishes the @jagreehal/* scope and awaitly. That single-account concentration is exactly what you would expect from a worm that enumerates and republishes everything one compromised account can touch:
autotel-*family (24 packages, plusautotelitself): includingautotel-mcp(with versions ranging across0.1.14through29.0.1),autotel-subscribers,autotel-terminal,autotel-mongoose,autotel-eventcatalog,autotel-devtools,autotel-aws,autotel-cloudflare,autotel-hono,autotel-playwright,autotel-sentry, and moreeslint-plugin-executable-stories-*family (Snyk has covered ESLint-plugin npm supply-chain malware before)@jagreehal/*packages@evolvconsulting/evolv-coder-lite
Many of the inflated version numbers (for example, autotel-mcp jumping into the 29.x range, or autotel getting both a 2.26.4 and a 3.4.3 published within the same minute on June 4) are themselves a side effect of the worm's automated republishing, not legitimate releases. The full, continuously updated list of packages and affected versions is on the Snyk incident page.
One important detail for remediation: for at least some packages, the latest dist-tag has since been pointed back at a clean release (for autotel, latest now resolves to 3.4.2, published before the malicious versions), but the malicious versions have not been unpublished. As of writing, autotel@3.4.3 and autotel@2.26.4 still return live tarballs. A fresh npm install autotel may pull the clean version, while any lockfile, exact pin, or transitive dependency that references a malicious version will still fetch the malware. Do not treat a clean latest tag as evidence you are safe.
How the attack works
The novel part: install-time execution through binding.gyp
When you run npm install on a package that contains a binding.gyp file and no matching prebuilt binary, npm hands the package to node-gyp and runs node-gyp rebuild to compile what it assumes is a native C/C++ addon. This is normal, expected behavior for any package with native components, and it happens without a preinstall or postinstall entry anywhere in package.json.
GYP's build configuration syntax supports command expansion. The <!(...) form runs a shell command during the configure phase and substitutes its output into the build definition. The compromised packages abuse this directly. Here is the exact 157-byte binding.gyp shipped in @vapi-ai/server-sdk@1.2.2 and autotel@3.4.3 (byte-for-byte identical across the packages we inspected):
The <!(node index.js ...) expression executes node index.js while node-gyp is merely configuring the build, long before any compiler runs. The "type": "none" target means nothing is actually compiled, so the command expansion's side effect (running index.js) is the entire point. Output is redirected to /dev/null so the install looks clean, and echo stub.c returns a plausible source filename so gyp continues without obvious error. The result is arbitrary code execution during a routine npm install.
Crucially, the package.json in these tarballs contains no preinstall, postinstall, install, or prepare scripts. The only scripts entries are ordinary development tasks (build, lint, test, format), and the package does not even declare "gypfile": true. There is nothing in package.json for script-focused tooling to flag; the presence of a binding.gyp file is sufficient for npm to invoke node-gyp on its own.
This is why the technique matters: a large amount of supply chain defense, including many --ignore-scripts assumptions, is built around the idea that install-time execution comes from lifecycle scripts. node-gyp invocation is a separate path that fires for any package with a binding.gyp.
The payload: a multi-stage Bun-based loader
The index.js that binding.gyp triggers is a 4.5 MB obfuscated loader. We decoded the outer layers statically from the published tarball (no execution) and confirmed the following chain:
ROT-14 Caesar cipher. The entire file is a single
evalover a ~1.3 million entry character-code array, mapped to a string and rotated by 14. The visible wrapper is literallyeval(function(s,n){return s.replace(/[a-zA-Z]/g,...rotate...)}([...],14)).AES-128-GCM self-decrypting layer. The decoded stage is an
asyncIIFE that importsnode:cryptoand defines anaes-128-gcmdecryptor (createDecipherivwith a 16-byte auth tag), then decrypts two embedded ciphertext blobs whose hex key, IV, and auth tag are hardcoded inline.Bun runtime loader (907-byte blob). The first decrypted blob detects OS and architecture, then downloads a standalone Bun v1.3.13 binary from the official
oven-sh/bunGitHub releases into a temp directory and runs it:
Main payload (~649 KB blob). The second decrypted blob (664,535 bytes) is the stealer logic itself, executed under the downloaded Bun binary rather than the Node.js process that started the install.
Running the core logic under a downloaded Bun binary, rather than the node process that started the install, is a deliberate evasion: monitoring scoped to Node.js child processes during npm install will not see the Bun process doing the real work. It also explains why a plaintext-string scan of index.js turns up no credentials or C2 indicators; that behavior lives inside the Bun-executed blob. We did not execute that final Bun stage, so the behavioral catalog below reflects publicly reported analysis of it.
Credential harvesting
Once executed, the payload sweeps developer and CI/CD environments for secrets, targeting:
AWS:
aws_access_key_id/aws_secret_access_key, and the IMDSv2 metadata endpoint (169.254.169.254)GCP:
GOOGLE_APPLICATION_CREDENTIALSand service account keysAzure: managed identity tokens via IMDS
GitHub Actions:
ACTIONS_ID_TOKEN_REQUEST_TOKENplus runner process memory scrapingHashiCorp Vault and Kubernetes: service account tokens from standard paths
Password managers: 1Password,
pass, andgopassstores
In GitHub Actions, the payload scrapes the runner's process memory to pull masked secrets back out in unmasked form, using a pattern like:
GitHub Actions secret masking redacts secrets in logs; it does not protect them from a process that can read runner memory directly. Any secret the runner can see should be treated as exposed.
Exfiltration through GitHub repositories
Rather than a fixed C2 domain, the malware uses GitHub itself as both rendezvous and dead drop. The activity has been tied to the GitHub account [liuende501](https://github.com/liuende501), which at the time of writing hosts 321 public repositories (api.github.com/users/liuende501), consistent with the worm's pattern of programmatically creating repositories to receive stolen data. The flow:
Discover the rendezvous by searching public commits for a hardcoded keyword
Create a repository on the fly with a randomized name
Upload encrypted harvested data as
results/results-{timestamp}.jsonIssue API requests with a
python-requests/2.31.0User-Agent
Using GitHub for exfiltration blends the traffic in with normal developer and CI activity, since outbound connections to github.com and api.github.com are rarely blocked in build environments.
Worm propagation across ecosystems
The payload is self-spreading, with separate engines per ecosystem:
npm worm: enumerates a maintainer's packages via
registry.npmjs.org/-/v1/search?text=maintainer:{username}, downloads each target, injects the maliciousbinding.gypandindex.js, and republishes. Consistent with earlier waves in this family (see Snyk's TanStack coverage, the first documented malicious npm package carrying valid SLSA provenance), the worm also forges Sigstore provenance attestations through Fulcio and Rekor so reinfected packages can appear legitimately signed.RubyGems worm: injects equivalent logic into
extconf.rb, RubyGems' native-extension build hook, reusing the same Bun downloader.extconf.rbis to RubyGems whatbinding.gypis to npm: a build-time file that runs automatically and is not a "script" in the lifecycle sense.GitHub repository poisoning: commits backdoor files into repositories the stolen tokens can write to, including AI coding agent and editor hooks (
.claude/,.cursor/rules/,.vscode/tasks.json) that re-execute the payload when a developer opens the project.
The cross-ecosystem reach (npm and RubyGems) and the reuse of build-time extension files in both is the throughline of this campaign: find the file that runs automatically at install or build time but that nobody classifies as a "script."
Impact analysis
The direct blast radius is any developer machine or CI runner that ran npm install and resolved one of the affected package versions. CI/CD environments carry the most risk, because the runner-memory scraping means every secret the runner can access, not only those passed as explicit environment variables, should be considered compromised.
Developer machines carry a secondary, longer-lived risk through the editor and AI-agent hooks, which survive a plain npm uninstall and re-execute the payload on the next session.
The likelihood of further spread appears lower than at the campaign's peak: the affected packages trace back to a small number of maintainer accounts (we confirmed all 25 autotel and autotel-* packages share the single account jagreehal), and no new compromised releases have been observed recently. That said, the malicious versions remain installable from npm, so exposure risk persists for anyone who pulls them, directly or transitively, before they are fully removed.
Detection
Scan with Snyk. Snyk has published advisories for the affected versions in the Snyk Vulnerability Database and stood up an incident page at security.snyk.io/node-gyp-supply-chain-compromise-june-2026. Run a scan against your project:
To scan against a specific manifest or lockfile:
Look for the technique, not just the package names. Because the execution path is binding.gyp, you can hunt for it independently of the package list:
Network and behavioral indicators:
node-gyp rebuildis running for packages that have no legitimate native addonUnexpected child processes (
curl,unzip,bun) spawned duringnpm installA standalone
bunbinary is being downloaded fromoven-sh/bunreleases mid-install when you never asked for BunGitHub API calls with a
python-requests/2.31.0User-Agent originating from a CI step that is not a Python process
Remediation
If you are unsure whether you were affected, treat it as a confirmed compromise. The harvested data is encrypted before exfiltration, so you cannot determine after the fact what was taken.
Step 1: Remove persistence before rotating tokens. If editor or AI-agent hooks were planted, remove them first so they cannot react to credential changes:
Step 2: Clean and reinstall on known-good versions.
Note that --ignore-scripts blocks lifecycle scripts but does not by itself stop a node-gyp build triggered by a binding.gyp. The reliable protection is to not install the malicious versions at all (pin to known-good versions) and to scan before building. Combine pinning with --ignore-scripts as a second layer.
Step 3: Rotate every credential reachable from an affected machine or runner:
npm publish tokens
GitHub personal access tokens and Actions secrets (repository and organization scoped)
AWS access keys and any IAM roles reachable from affected runners
GCP service account keys
Azure service principals and managed identity scopes
HashiCorp Vault tokens
Kubernetes service account tokens
Anything in a targeted password manager store (1Password,
pass,gopass)
Step 4: Audit GitHub for injected workflows and dead-drop repos.
Step 5: Harden against the next wave.
Default to
npm install --ignore-scriptsin CI (best practice, to protect against the wider family of install-time attacks) and pair it with a scanning gatePin dependencies to exact versions with lockfile integrity hashes
Consider a registry cooldown policy that holds packages published in the last several days before allowing them into builds
Apply least-privilege scoping to CI/CD tokens so a single harvested token has a small blast radius
Use Snyk to continuously monitor your dependency tree for malicious packages as they are flagged
Snyk's guide to preventing npm supply chain attacks covers the broader checklist.
The bigger picture: A Shai-Hulud descendant
This is the latest wave in the Shai-Hulud / Miasma lineage, a family of self-propagating npm worms that has hit the registry repeatedly since late 2025. Snyk covered an earlier June 2026 Miasma wave that hit Red Hat npm packages; the exfiltration repositories used across these waves carry descriptions that reference the earlier Shai-Hulud campaigns directly. Each wave has reused a Bun-runtime obfuscated stealer and extended it with new persistence, new exfiltration routes, and new ways to fire code automatically at install or build time. The progression of that automatic-execution trick is the part worth internalizing:
Earlier waves leaned on
preinstall/postinstalllifecycle scriptsSubsequent waves added AI coding agent hooks (
SessionStart) and IDE folder-open tasks for persistenceThis wave moves the initial execution to
binding.gyp/node-gyp(andextconf.rbon RubyGems), build-time files that are not lifecycle scripts at all
Snyk has covered prior waves of this campaign family in depth:
For an overview of the worm family this incident descends from:
Snyk security researcher explaining the Mini Shai-Hulud npm supply chain worm and how it propagates Mini Shai-Hulud: The Most Sophisticated NPM Supply Chain Attack of 2026 (Overview of the Shai-Hulud / Miasma worm family and how it self-propagates)
For a hands-on walkthrough of finding and remediating compromised packages from this campaign family with Snyk:
Snyk security engineer demonstrating how to identify and remediate Shai-Hulud compromised npm packages using the Snyk CLI Shai-Hulud NPM Attack: Remediation with Snyk - Walkthrough of identifying and remediating compromised packages using Snyk tooling.
For background on how compromising a legitimate, trusted package fits into the broader supply chain threat model, Snyk Learn's lesson on Compromise of Legitimate Packages is a useful primer.
Timeline (UTC)
Date | Event |
|---|---|
June 1, 2026 | Earlier Miasma variant compromises a separate cluster of npm packages (Red Hat related) |
June 3, 2026 | Primary wave: |
June 3, 2026 (onward) | Public technical analysis of the "Phantom Gyp" technique published; Snyk publishes advisories and a live incident page |
Ongoing | Investigation continues; malicious versions remain available on npm pending removal |
Snyk coverage
Snyk has published advisories for the affected versions in the Snyk Vulnerability Database and maintains a live incident page listing all 57 packages and their malicious versions. Snyk customers can use Snyk's scanning across the SDLC to identify which projects pull affected versions, directly or transitively, and prioritize remediation based on exposure.
Check out the Snyk Vulnerability DB
Trusted data and actionable insights to help you build software securely.
