Artificer Digital The Artificer's Grimoire

Scout: The Agent Substrate Attack Surface — GitHub, Grafana, and the Shai-Hulud Return

Summary

In a single eight-day window in May 2026, the same threat cluster — tracked as TeamPCP, running a worm the research community calls Mini Shai-Hulud — chained an IDE extension, an npm CI/CD compromise, and a maintainer-account takeover into three named breaches: GitHub’s internal repositories (~3,800 exfiltrated), Grafana’s full codebase, and 300-plus AntV npm packages. The throughline is not a novel zero-day. It is the exploitation of inherited trust across the exact substrate that coding agents run on: VS Code Marketplace and Open VSX extensions, npm lifecycle scripts, GitHub Actions OIDC tokens, and — newly, and specifically — the credential files that agent harnesses write to disk, including ~/.claude/settings.json. Two facts make this wave qualitatively worse than the March 2026 TeamPCP campaign that hit LiteLLM. First, Grafana rotated GitHub workflow tokens the same day it detected the intrusion and still lost its codebase, because one missed token was enough — proving “rotate on detection” is not a defensible cadence. Second, the Mini Shai-Hulud worm publishes malicious packages carrying valid SLSA Build Level 3 provenance attestations, because it mints publish credentials from inside the legitimate CI workflow rather than stealing a long-lived token — which means provenance verification, the control the industry has been pushing for two years, no longer distinguishes good from evil on its own. What follows turns those facts into an audit checklist for teams running agent harnesses in CI and on developer machines.

Key Findings

1. The attack chain runs straight through coding-agent substrate

The mechanically important detail is that every link in the May chain is something a coding-agent deployment touches by default.

The entry point was a single developer’s GitHub personal access token, leaked when the TanStack npm packages were poisoned and that developer’s machine ran the malware. That stolen token was used to push an unsigned orphan commit to the nrwl/nx repository, which in turn poisoned the Nx Console VS Code extension (nrwl.angular-console, version 18.95.0, ~2.2 million installs). The trojanized extension was live on the Visual Studio Marketplace for roughly 11–18 minutes on May 18, 2026 — about 11 minutes by StepSecurity’s 12:36–12:47 UTC timeline, up to 18 by The Hacker News’ 12:30–12:48 UTC window; StepSecurity reports Open VSX was not affected (StepSecurity; The Hacker News). When a developer opened any workspace, the extension silently ran a dropper that harvested credentials and exfiltrated them. GitHub disclosed on May 19–20 that approximately 3,800 of its internal repositories were exfiltrated from a compromised employee device, and GitHub CISO Alexis Wales identified the poisoned Nx Console extension as the vector (GitHub Security; Help Net Security). The threat actor, TeamPCP, then attempted to sell the stolen data (StepSecurity).

What the dropper collected is the part worth dwelling on. StepSecurity’s analysis enumerates six credential-collector classes spanning GitHub tokens (ghp_, gho_, ghs_), npm OIDC tokens, AWS credentials via IMDS/ECS metadata, HashiCorp Vault tokens, Kubernetes service-account tokens, 1Password CLI vault items, SSH keys — and AI assistant credentials read directly from ~/.claude/settings.json (StepSecurity). The Hacker News’ summary of the harvested set lists “1Password vaults, Anthropic Claude Code configurations, npm, GitHub, and Amazon Web Services” in the same breath (The Hacker News). StepSecurity flags this as possibly one of the first supply-chain payloads built to harvest AI coding-assistant credentials and configuration — ~/.claude/settings.json swept in alongside the GitHub, npm, AWS, Vault, and SSH credentials rather than singled out (StepSecurity). Either way the practitioner consequence is the same: a developer machine running Claude Code, an MCP server with provider keys, and a verified-publisher IDE extension is now a single-compromise path to the whole credential portfolio.

Reporting names OpenAI, Grafana, and Mistral AI among the organizations caught downstream of the same TanStack-rooted compromise (The Hacker News; Aikido). GitHub’s own advisory confirms only its internal-repository exfiltration and states it has “no evidence of impact to customer information stored outside of GitHub’s internal repositories” (GitHub Security).

2. “Rotate on detection” is not a defensible OIDC rotation cadence — Grafana proves it

Grafana Labs detected malicious activity on May 11 and, by its own account, “immediately initiated our incident response plan… performed analysis and quickly rotated a significant number of GitHub workflow tokens, but a missed token led to the attackers gaining access to our GitHub repositories.” A later review found that “a specific GitHub workflow we originally deemed not impacted had, in fact, been compromised” (Grafana; SecurityWeek). The attackers downloaded Grafana’s public and private source code (not altered, only exfiltrated), issued a ransom demand on May 16, and Grafana declined to pay, citing the FBI’s position that paying does not guarantee security (Grafana).

The practitioner lesson is sharp: same-day, human-driven rotation of “a significant number” of tokens still left a gap, and one gap was sufficient. Detection-triggered rotation is a race the defender loses because it depends on enumerating every credential the blast radius touched — under incident pressure, against an attacker who already has a foothold. The defensible posture is to make the credentials themselves short-lived enough that detection-time rotation is mostly moot: workflow-scoped, ephemeral OIDC credentials that expire on the order of minutes, not stored tokens that a responder must remember to revoke. This is exactly the direction npm’s Staged Publishing change pushes (Finding 5), and it is why “we rotate on detection” should be treated as an incident-response fallback, not a control.

3. Valid provenance no longer means safe: the worm forges nothing — it mints

The TanStack compromise is the cleanest published anatomy of how a modern npm worm defeats provenance. The TanStack maintainer account was never directly compromised. Instead the attacker chained three GitHub Actions weaknesses (TanStack postmortem; gblock analysis):

  1. A pull_request_target Pwn Request. The bundle-size.yml workflow ran on pull_request_target — which grants the base repository’s write token and secrets — and explicitly checked out refs/pull/{pr}/merge, i.e. the untrusted fork code. A single PR from a fork (voicproducoes/router, PR #7378) executed attacker code with base-repo privileges. GitHub’s Security Lab has warned for years that “combining pull_request_target workflow trigger with an explicit checkout of an untrusted PR is a dangerous practice that may lead to repository compromise” (GitHub Security Lab) — and researchers have used exactly this single-PR-from-a-fork pattern to compromise repositories owned by Microsoft, Google, and Nvidia (Orca Security).
  2. Cache poisoning across the fork/base trust boundary. The fork wrote an obfuscated payload into the pnpm store before the cache action uploaded it. Because “two workflows in the same repository, even running from different branches, share cache scope as long as the cache key matches,” the attacker reverse-engineered the exact cache key and seeded the poison hours ahead of the release workflow, which restored it (gblock).
  3. OIDC token extraction from runner memory. The release workflow’s short-lived OIDC publish token sat in the Runner.Worker process as a JSON blob. Under ~50 lines of Python the payload opened /proc/{pid}/mem, scanned for the pattern, and extracted it — bypassing GitHub’s log-masking entirely, which only scrubs logs, never memory (gblock).

The payload then called npm’s OIDC exchange endpoint directly and published 84 malicious versions across 42 @tanstack/* packages between 19:20 and 19:26 UTC on May 11 (TanStack postmortem). Crucially, because the publish came from inside TanStack’s legitimate workflow identity, “the resulting packages carried genuine SLSA provenance signatures, since the npm registry correctly attested they came from TanStack’s workflow — unaware the cache had been poisoned” (gblock). Reporting describes this as the first documented npm worm producing validly-attested malicious packages, and the wider campaign expanded to roughly 172 packages across 403 malicious versions spanning npm and PyPI within 48 hours (VentureBeat).

For teams running release pipelines, this is the architectural update that matters most coming out of the May wave. Provenance attestation answers “did this artifact come from the build it claims to?” — it does not answer “was that build itself trustworthy at the moment it ran?” A poisoned cache makes a clean-looking workflow emit a validly-signed malicious artifact. Provenance verification remains worth doing, but it can no longer be the terminal control.

4. The worm self-propagates through maintainer accounts and carries a dead-man switch

Sonatype’s framing of the late-May AntV wave is blunt: “When attackers compromise real maintainers and publish malware under real package names, traditional trust signals are obsolete. The package looks safe because yesterday it was” (Sonatype). The AntV incident compromised the publishing credentials of maintainer account atool (which manages 547 packages including the @antv visualization suite) plus a second account, and pushed on the order of 300-plus legitimate packages into malicious versions (JFrog; Snyk).

The propagation mechanic is the worm’s defining property. After harvesting credentials it enumerates every other package the victim maintains — via registry.npmjs.org/-/v1/search?text=maintainer:<user> — and republishes them, locating a publishable npm token with bypass_2fa set to true to do so (TanStack postmortem). The install-time delivery uses a preinstall lifecycle hook ("preinstall": "bun run index.js") running a ~499 KB obfuscated Bun bundle; Bun matters because it is a self-contained runtime that executes the payload even where the local Node install is constrained (JFrog; Picus). The payload daemonizes a detached background copy so the install appears normal while the malicious logic continues.

One operational subtlety changes incident response: the AntV variant ships a destructive dead-man switch. JFrog flags an embedded threat string — IfYouInvalidateThisTokenItWillNukeTheComputerOfTheOwner — meaning revoking the stolen GitHub token while persistence is still live can trigger destructive commands. The remediation ordering is therefore isolate the host first, then revoke credentials (JFrog). Teams whose runbooks default to “rotate tokens immediately” need to invert that step for this class of payload.

5. The registry-side controls have moved — and they target exactly this attack

npm shipped two responses in May 2026 that are directly aimed at the stolen-CI-token vector, and both are now table stakes for any team that publishes packages.

Staged Publishing (now GA). Instead of publishing straight to the public registry, a tarball goes into a staging queue and “a human maintainer with a 2FA challenge is required to approve a staged package before it is released to the registry” (GitHub Changelog). The approval cannot be performed by an OIDC token alone, which reintroduces human proof-of-presence even into trusted-publishing CI flows (Socket; The Hacker News). It requires npm CLI 11.15.0+ and npm stage publish; trusted-publishing configs can be set “stage-only” so a raw npm publish from CI is rejected (GitHub Changelog). This is the control that would most directly have blunted the TanStack vector, had it been in place and set stage-only: the stolen OIDC token could mint a staged publish, but not clear the human 2FA gate. The caveat is that Staged Publishing only reached GA on May 22 — after the incident — and is opt-in, so it shields only the projects that adopt it and lock their trusted-publishing config to stage-only.

Granular-token invalidation. On May 19, npm “invalidated npm granular access tokens with write access that bypass 2FA” — the exact token class the worm hunts for to self-propagate (Socket).

New install-time allow-flags. npm 11.15.0 added --allow-file, --allow-remote, and --allow-directory (joining --allow-git), each accepting all or none, to constrain non-registry install sources (GitHub Changelog). And on the consumer side, minimumReleaseAge is now the most direct lever for avoiding a freshly-poisoned version: pnpm 11 ships a one-day default window, and the setting exists across npm, Yarn, and Bun (Socket). A release-age gate of even 24 hours would have excluded the TanStack window entirely.

6. IDE extensions are an under-governed install surface — and AI forks make it worse

The Nx Console breach is a specific instance of a structural problem Wiz quantified earlier in 2026: across 500-plus extensions they found 550-plus validated secrets, including “over one hundred valid leaked VSCode Marketplace PATs” and AI-provider keys for OpenAI, Gemini, and Anthropic (Wiz). The danger amplifier is auto-update: VS Code updates extensions by default, so an attacker who gains an extension’s publish rights can push malware straight to the entire install base of that extension with no further action from the victim. Wiz’s recommendation set is the closest thing to a marketplace-governance baseline: inventory installed extensions, run a centralized allowlist, prefer the VS Code Marketplace over Open VSX because it currently has higher review rigor, and vet publishers on reputation and install prevalence before adoption (Wiz).

The agent-specific twist: AI-powered VS Code forks like Cursor and Windsurf source extensions from Open VSX, the less-scrutinized registry (Wiz). The GlassWorm campaign — self-propagating malware first documented spreading across Open VSX in late 2025, using printable Unicode characters that don’t render in the editor to hide its code — has explicitly mimicked AI-coding-assistant extensions, with at least 72 malicious Open VSX extensions found since January 2026 (The Hacker News). Teams that standardized on a VS Code fork for agent work inherited a weaker extension-vetting story than the teams still on upstream VS Code, and most have not audited the difference.

Practical Implications

Immediate (this week)

  1. Hunt for the May payload on developer machines. On macOS, check for ~/.local/share/kitty/cat.py and the com.user.kitty-monitor LaunchAgent; kill __DAEMONIZED processes (StepSecurity). Isolate the host before revoking any GitHub token — the AntV variant’s dead-man switch nukes hosts on token invalidation (JFrog).
  2. Treat ~/.claude/settings.json and equivalent agent-config files as harvested. Any machine that ran Nx Console 18.95.0 (or any extension flagged in this wave) should have its Anthropic, OpenAI, and MCP-server provider keys rotated, alongside the usual GitHub/AWS/npm/Vault/Kubernetes set (StepSecurity).
  3. Inventory every VS Code / Open VSX extension across the dev fleet, including on Cursor/Windsurf installs. Flag anything published or updated in the trailing 48 hours for hold-and-review (Wiz; Aikido).

Short-term hardening (next 30 days)

  1. Kill pull_request_target + untrusted checkout. Audit every workflow for pull_request_target that checks out refs/pull/*/merge or fork head. Split untrusted build (pull_request, no secrets) from trusted post-processing (workflow_run, passing only artifacts), and gate privileged jobs behind if: github.repository_owner == '<org>' (GitHub Security Lab; gblock).
  2. Treat CI cache as untrusted input. Hash and verify any artifact restored from Actions cache before consuming it; do not let a build job and a release job share a predictable cache scope (gblock).
  3. Enable a minimumReleaseAge gate (pnpm 11 defaults to one day; configure the equivalent on npm/Yarn/Bun) on every project, including the lockfiles your agent harness installs from (Socket).
  4. Adopt npm Staged Publishing for anything you publish. Move to npm CLI 11.15.0+, set trusted-publishing configs to stage-only, and require the 2FA human approval gate so a stolen OIDC token can’t ship a release on its own (GitHub Changelog).
  5. Establish a VS Code extension allowlist and prefer the VS Code Marketplace over Open VSX where your editor permits it; for Cursor/Windsurf fleets, document the Open VSX exposure explicitly (Wiz).

Strategic (next quarter)

  1. Re-architect publish credentials to be ephemeral, not rotatable. The defensible answer to Grafana’s missed token is to not have a long-lived token to miss: workflow-scoped OIDC credentials that expire in minutes, gated by Staged Publishing, so detection-time rotation is a fallback rather than the primary control (Grafana; GitHub Changelog).
  2. Stop treating provenance as a terminal control. SLSA attestation tells you an artifact came from the build it claims; it does not tell you the build was clean. Pair provenance verification with build-environment integrity (cache verification, isolated release runners, no untrusted code in privileged contexts) — because the worm now ships validly-attested malware (gblock; VentureBeat).
  3. Bring agent-config files into the secrets-management perimeter. Provider keys for coding agents belong in a secrets manager with short-lived retrieval, not in plaintext ~/.claude/settings.json or shell-profile environment variables — the same credential-isolation discipline already applied to LLM-proxy layers now extends to the agent harness itself.

Coding-agent infrastructure audit checklist

  • Every VS Code / Open VSX extension on the dev fleet (including Cursor/Windsurf) is inventoried and on an allowlist
  • Extensions published/updated in the trailing 48h are held for review before auto-update applies
  • Agent-config files (~/.claude/settings.json, MCP server configs) hold no long-lived provider keys; keys come from a secrets manager
  • No workflow combines pull_request_target with checkout of untrusted PR code
  • Privileged/release jobs are split from untrusted-build jobs and guarded by a repository_owner check
  • CI cache artifacts are hash-verified before consumption; build and release jobs don’t share cache scope
  • minimumReleaseAge (≥24h) is enforced on every project the harness installs from
  • Published packages go through npm Staged Publishing with a 2FA human approval gate
  • Publish credentials are ephemeral OIDC, not stored tokens; no bypass_2fa write tokens exist
  • Incident runbook orders isolate-then-revoke for install-time-script compromises
  • GitHub Actions pinned to commit SHAs, not floating tags

Open Questions

  1. Does provenance get a second layer, or get demoted? With validly-attested malicious packages now in the wild, the open question is whether the ecosystem adds build-environment attestation (proving the runner was uncompromised, not just the identity) or whether provenance quietly loses its status as a trust signal. No standard for “this build ran in an uncompromised environment” exists yet.

  2. What is the right release-age window for agent harnesses specifically? Agents that auto-install dependencies mid-task have a different risk profile than a human running npm install once. A 24-hour gate that’s fine for a human workflow may be unworkable for an agent that needs a just-published fix — and the calibration data doesn’t exist.

  3. Can the bypass_2fa token class be eliminated, not just invalidated once? npm’s May 19 invalidation was a one-time reset. Whether the registry deprecates the token class entirely — versus letting it regenerate — determines whether the worm’s primary propagation vector returns.

  4. Who governs Open VSX security as AI forks scale? Cursor, Windsurf, and other forks have pushed Open VSX into a critical-path position it wasn’t resourced for. Reporting on who funds and staffs Open VSX’s security review at this new scale remains thin.

  5. How much agent-harness credential exposure is plaintext-on-disk by default? The campaign’s harvest of ~/.claude/settings.json worked because the config was readable plaintext. How many agent tools write provider keys to disk in the clear, and whether the tooling moves to OS keychains, is undocumented across the field.

Sources

  1. GitHub Security, “Investigating unauthorized access to GitHub’s internal repositories”
  2. Grafana Labs, “Security update: latest on the TanStack npm supply chain ransomware incident”
  3. SecurityWeek, “Grafana Says Codebase and Other Data Stolen via TanStack Supply Chain Attack”
  4. TanStack, “npm supply chain compromise — postmortem”
  5. Sonatype, “Shai-Hulud is back: maintainers are the target”
  6. Help Net Security, “GitHub and Grafana breaches share a root cause: a poisoned Nx Console extension”
  7. StepSecurity, “Nx Console VS Code Extension Compromised”
  8. The Hacker News, “GitHub Internal Repositories Breached via Malicious Nx Console VS Code Extension”
  9. gblock, “TanStack npm Attack: 84 Versions Published via OIDC Token Theft and Cache Poisoning”
  10. GitHub Security Lab, “Keeping your GitHub Actions and workflows secure Part 1: Preventing pwn requests”
  11. Socket, “npm Invalidates Granular Access Tokens as Mini Shai-Hulud Spreads”
  12. GitHub Changelog, “Staged publishing and new install-time controls for npm”
  13. JFrog Security Research, “Shai-Hulud Returns: npm worm hits @antv in latest ongoing campaign”
  14. Picus Security, “Mini Shai-Hulud: The npm Supply Chain Worm Explained”
  15. VentureBeat, “Protect your enterprise from the Shai-Hulud worm: 172 npm/PyPI packages, valid provenance, CI/CD audit”
  16. Wiz, “Supply Chain Risk in VSCode Extension Marketplaces”
  17. The Hacker News, “GlassWorm Supply-Chain Attack Abuses 72 Open VSX Extensions to Target Developers”
  18. Aikido, “GitHub Breached via VS Code Extension”
  19. The Hacker News, “npm Adds 2FA-Gated Publishing and Package Install Controls Against Supply Chain Attacks”
  20. Snyk, “Mini Shai-Hulud Hits AntV: 300+ Malicious npm Packages Published via Compromised Maintainer Account”
  21. The Hacker News, “Mini Shai-Hulud Worm Compromises TanStack, Mistral AI, Guardrails AI & More Packages”
  22. StepSecurity, “Mini Shai-Hulud is Back: A Self-Spreading Supply Chain Attack Hits the npm Ecosystem”
  23. Orca Security, “pull_request_nightmare Part 2: Exploiting GitHub Actions for RCE and Supply Chain”
  24. GitHub Changelog, “Actions pull_request_target and environment branch protections changes”