Safeguard
Supply Chain Security•

Phantom Gyp: npm worm runs from binding.gyp, not install scripts

On 3 June 2026 a worm hit 57 npm packages without a single preinstall or postinstall script. It runs through binding.gyp and node-gyp instead. Here is how to find it.

Safeguard Security Team
6 min read

On 3 June 2026 an automated burst of malicious releases hit dozens of npm packages, starting with @vapi-ai/server-sdk and several families published by one maintainer. Snyk, which published its analysis on 4 June, tracks 57 affected packages and hundreds of malicious versions, rated Critical. What made it different: the package.json files contained no preinstall, postinstall, install or prepare script, so the usual advice to review lifecycle scripts found nothing.

StepSecurity named the install-time technique "Phantom Gyp" and tied it to the Miasma campaign, a descendant of the Shai-Hulud worm family.

What's affected

Snyk's incident page holds the full list. The examples it printed:

PackageMalicious versions
@vapi-ai/server-sdk0.11.1, 0.11.2, 1.2.1, 1.2.2
ai-sdk-ollama0.13.1, 1.1.1, 2.2.1, 3.8.5
autotel2.26.4, 3.4.3
awaitly1.33.3

The autotel and autotel-* packages (25 in all), the @jagreehal/* scope, awaitly, eslint-plugin-executable-stories-* and @evolvconsulting/evolv-coder-lite were also hit. Some version numbers look inflated because the worm republished automatically. For autotel, the latest tag was moved back to clean 3.4.2, but the malicious 3.4.3 and 2.26.4 stayed downloadable, so a clean tag does not mean a clean lockfile.

How the trick works

When npm install meets a binding.gyp file and finds no prebuilt binary, it runs node-gyp rebuild on its own (the tool is the node-gyp build helper). The malicious file was 157 bytes. It used a command expansion to run node index.js during configuration, set the target type to none so nothing compiled, and echoed a stub file name so the build looked normal.

The index.js was a multi-megabyte obfuscated file. Per Snyk, it decrypts a loader that downloads Bun v1.3.13 from the official oven-sh/bun releases and runs the main stealer under Bun. Running under a second runtime sidesteps monitoring that only watches Node.js child processes.

The stealer goes after AWS, GCP and Azure credentials and metadata endpoints, GitHub Actions tokens and runner memory, Vault and Kubernetes tokens, and password managers. It exfiltrates through GitHub repositories rather than a fixed server. It then enumerates the victim's other npm packages, injects the same files and republishes. Snyk also reports persistence in .claude/, .cursor/rules/ and .vscode/tasks.json, which survives npm uninstall.

Indicators of compromise

Snyk lists these signs. Any one of them on a build host is worth a closer look:

  • A binding.gyp that contains a <!( command expansion calling node index.js.
  • An index.js in a package root that is larger than 1 MB.
  • node-gyp rebuild running for a package that has no native addon.
  • A standalone Bun binary downloaded from the oven-sh/bun release page during npm install.
  • GitHub API calls with the user agent python-requests/2.31.0 from CI steps that are not Python.
  • Repositories or workflows that nobody on your team created. Snyk names the GitHub account liuende501 as associated with the exfiltration.

The same family also reached RubyGems. Snyk says the worm writes equivalent logic into extconf.rb, which is the Ruby counterpart of binding.gyp. If you publish gems from a machine that installed one of these packages, check those too.

How to check if you're exposed

Snyk's own detection commands:

grep -rl '<!(' node_modules/*/binding.gyp node_modules/**/binding.gyp
find node_modules -maxdepth 2 -name index.js -size +1M
ls -la .claude/ .cursor/rules/ .vscode/tasks.json

Then compare your lockfile against the affected versions above:

grep -E '"(autotel|ai-sdk-ollama|awaitly|@vapi-ai/server-sdk)"' package-lock.json
npm ls autotel ai-sdk-ollama awaitly @vapi-ai/server-sdk

Also look in CI logs for node-gyp rebuild on packages that have no native addon, and for curl, unzip or bun processes during install.

What to do now

  1. Look for injected hook files first (.claude/settings.json, .vscode/tasks.json, .cursor/rules/setup.mdc) and remove them. Snyk says to do this before rotating credentials, or new secrets can be taken again.
  2. Delete node_modules, pin to known-good versions and reinstall. Snyk notes that --ignore-scripts alone is not enough, because the build step is not a lifecycle script.
  3. Rotate everything the machine or runner could reach: npm tokens, GitHub tokens and Actions secrets, cloud keys, Vault and Kubernetes tokens.
  4. Review .github/workflows/ history and your repository list for workflows or repos you did not create.
  5. If you cannot tell whether you were hit, treat it as confirmed. The stolen data is encrypted before it leaves.

How this could have been avoided

  • Block the build path, not only the scripts. Pair --ignore-scripts with a check that fails CI when a dependency without a native addon ships a binding.gyp.
  • Use a dependency cooldown. The malicious versions were found within hours. A rule that refuses versions younger than a few days would have skipped them.
  • Limit what the runner can see. The stealer read cloud metadata and runner memory. Short-lived credentials and IMDSv2 hop limits shrink the haul.
  • Restrict egress in CI. Downloading a Bun binary from a release page during npm install is unusual and easy to block.
  • Protect the maintainer side. The worm spread through stolen publishing tokens, so scoped tokens and 2FA on publishing matter most for people who own many packages.

Why it matters

Defenders spent months learning to review postinstall. The attackers moved to a file that most scanners and reviewers treat as build plumbing. Each wave in this family has used a different execution hook: lifecycle scripts, then AI agent and editor hooks, then binding.gyp.

The lesson is to detect behavior at install time instead of one specific mechanism. Any file that can run code when a package lands is in scope.

Try it yourself

Pick one Node project and run the three grep and find commands above. Then list every dependency in node_modules that has a binding.gyp and ask whether it should have native code.

Click through it at your own pace, or open it full screen.

You can also paste your package.json into the free dependency scan. It runs in your browser and the file never leaves your machine.

Quick check

Why did reviewing preinstall and postinstall scripts miss this attack?

The code ran through a malicious binding.gyp, which makes npm invoke node-gyp rebuild during install. The package.json had no lifecycle scripts at all.

Why is it not enough to run npm install with the ignore-scripts flag?

The build step is not a lifecycle script, and other tools may still build the package. Snyk also notes the pinned lockfile versions can still resolve to a malicious release.

Sources

Never miss an update

Weekly insights on software supply chain security, delivered to your inbox.

Self-healing security runs on Safeguard.

Your first fix PR is minutes away.

No sales call required, even your agent can complete the purchase over MCP.