top of page

Your --ignore-scripts Won't Save You: Phantom Gyp Backdoored 57 npm Packages Through a File Nobody Watches

  • Writer: Patrick Duggan
    Patrick Duggan
  • 4 minutes ago
  • 5 min read

For two years the standard advice for surviving a hostile npm install has been three words: run --ignore-scripts. Block the preinstall and postinstall hooks, the thinking goes, and the malware never gets to run. On June 3, 2026, a worm the researchers are calling Phantom Gyp walked straight around that advice and backdoored 57 packages — 286-plus malicious versions, published in a rolling burst under two hours — through a file your scanner has never been taught to look at.


That file is binding.gyp. And if you have spent any time near a Node project with native dependencies, you already have dozens of them on disk and have never once read their contents. That is the whole point.



The door that was always unlocked


Here is the mechanism, and it is worth slowing down for because it is the genuinely new part. When npm installs a package that ships a binding.gyp file, it automatically invokes node-gyp rebuild to compile the native add-on. This happens regardless of what is — or is not — declared in package.json. There is no preinstall script. No postinstall script. No prepare script. The packages in this campaign were clean by every lifecycle-hook measure a scanner checks.


The trick lives in GYP's own command-expansion syntax. GYP supports an inline form, written as an exclamation point and parentheses after a less-than sign, that executes a shell command and substitutes its output back into the build definition. The attackers put their payload there. The compromised file declared its sources as the output of running node index.js, piping everything to /dev/null, and only then echoing a stub filename. The build target was typed "none" — nothing was ever actually compiled. The compilation was never the goal. The command substitution running attacker code during the configure phase, before a single byte gets built, was the entire objective.


That is why this matters more than another worm headline: npm install --ignore-scripts does not block node-gyp builds triggered by binding.gyp. The defense everyone reached for after the last year of supply-chain carnage does not cover this path. Snyk and StepSecurity both confirmed it independently. The only reliable mitigation is the unglamorous one — pin your dependencies to known-good versions and never resolve the poisoned releases at all.



A 157-byte file with a 4.5-megabyte friend


The binding.gyp itself is 157 bytes. The index.js it summons is about 4.5 megabytes, and it is a small museum of evasion. The outer wrapper is a ROT-N Caesar cipher — the rotation value drifted between samples, ROT-9 through ROT-20, presumably to defeat naive signature matching. Under that sits an AES-128-GCM layer self-decrypting from inline hex blobs with hardcoded keys. Under that, a 907-byte loader downloads a standalone Bun runtime, version 1.3.13, pulled from Bun's own official GitHub releases. The real stealer — roughly 649 kilobytes — then runs under Bun instead of Node.


Read that last move twice. Any monitoring you have scoped to "Node child processes spawned during install" goes blind the moment the payload re-hosts itself under a different runtime it fetched mid-install. This is the same instinct we wrote about with Shai-Hulud V2 — the worm authors are no longer hiding the payload, they are hiding the execution context.



What it steals, and the part that should worry CI/CD teams


The stealer targets exactly what you would expect and a few things you would not: AWS keys and IMDS tokens off the 169.254.169.254 endpoint, GCP service accounts and Secret Manager, Azure managed-identity tokens and Key Vault, HashiCorp Vault, Kubernetes service tokens, and the local password-manager stores for 1Password, pass, and gopass.


The novel piece is what it does to GitHub Actions runners. GitHub masks secrets in logs — that is the safety net everyone trusts. Phantom Gyp scrapes the runner's process memory directly, using a pattern that strips null bytes and greps for the JSON shape GitHub uses to mark a value as a secret, and pulls those secrets back out unmasked. The masking is a log-redaction feature, not a memory-protection feature, and the worm knows the difference.



Then it poisons the tools that write your code


This is the part that hit closest to home for us, because we run on these tools every day. Using stolen tokens, the worm commits backdoor files into the repositories it can reach — and not just any files. It drops a .claude/setup.mjs for Anthropic's Claude Code, a .cursor/rules/setup.mdc for Cursor, and a .vscode/tasks.json for VS Code. These execute when a developer next opens the project. The compromise is no longer in the package you installed. It is in the assistant that helps you write the next package.


We have been tracking this convergence for weeks. When TeamPCP's Mini Shai-Hulud started targeting Claude Code specifically, we said the agent layer was becoming the persistence layer. Phantom Gyp is that prediction with a build-time delivery mechanism bolted on. Defend the door, not just the actor — the door here is every auto-execution path your editor and your CI runner expose.



The forged-trust playbook, again


The self-propagation engine enumerates every package owned by a compromised maintainer account, injects the binding.gyp and index.js pair, and republishes. To make the reinfected versions look legitimate, it requests signing certificates from Fulcio, writes transparency entries to Rekor, and forges SLSA v1 provenance attestations. A RubyGems variant does the identical thing through extconf.rb, Ruby's native-extension build hook.


If forged Sigstore provenance sounds familiar, it should — we documented Shai-Hulud V3 forging SLSA attestations across 416 packages including TanStack, Mistral, Bitwarden, and SAP. Provenance proves a package was built by a pipeline. It does not prove the pipeline was honest. That distinction is going to define the next year of this fight, and it is exactly why npm Trusted Publishing has to be paired with cooldown windows on freshly published versions, not treated as a finish line.



What to actually do


Pin your dependencies and do not resolve recently published versions until they have aged — a registry cooldown of even 24 to 72 hours strands most of these rolling bursts, because the malicious releases get reverted on the latest dist-tag but, critically, are almost never unpublished and stay installable in lockfiles forever. Treat --ignore-scripts as a second layer, not the wall. Flag any non-entry-point file over a megabyte, any node-gyp rebuild for a package with no real native add-on, any /tmp/b-star directory holding a Bun binary, and any curl or unzip spawned as a child of npm install. Watch your .claude, .cursor, and .gemini directories for files you did not write. And rotate every credential a poisoned install could have touched — cloud keys, npm tokens, GitHub PATs — on the assumption it already saw them.


The published indicators include the binding.gyp hash beginning ef641e95 and the decrypted payload hash beginning da39146e, with exfil running through a GitHub account hosting 236 programmatically created repositories, several still labeled "Miasma — The Spreading Blight."


We indexed the Miasma campaign when it hit Red Hat's npm scope on June 1. We mapped Shai-Hulud V2's worm anatomy and V3's forged attestations. Phantom Gyp is the same lineage finding a quieter door. We are about 95 percent confident the binding.gyp path is the technique to watch next, because it converts a feature every native Node package depends on into an execution primitive your existing tooling was never built to question.


Related reading: our breakdown of Miasma backdooring 95 Red Hat npm packages, the anatomy of the Shai-Hulud V2 self-propagating worm, Shai-Hulud V3 forging SLSA attestations for 416 packages, and TeamPCP's Mini Shai-Hulud turning its attention to Claude Code.




The threat feed this post is built on

1.14M+ IOCs, STIX 2.1, precursor signals, supply-chain detection. Free API key in 30 seconds.


bottom of page