· Paul Lukic · 11 min read · securitysupply-chainai-coding-agentsnpmaudit-log

The TanStack npm Compromise: Why Every AI Coding Agent Needs an Audit Log

On 11 May 2026, 84 malicious versions of 42 @tanstack/* packages hit npm and started stealing AWS, GCP, Kubernetes, Vault, GitHub, and SSH credentials. If your AI coding agent ran npm install during that window, you may never know. Here's why agent command logging is now non-negotiable.

In this post

On 11 May 2026, between 19:20 and 19:26 UTC, 84 malicious versions of 42 @tanstack/* packages were published to the npm registry. Six minutes. That’s how long it took for an attacker to weaponize one of the most-downloaded JavaScript ecosystems on the planet.

The packages stole AWS credentials, GCP metadata, Kubernetes service-account tokens, Vault tokens, npm tokens, GitHub tokens, and SSH private keys. They exfiltrated everything to Session/Oxen messenger endpoints. GitHub assigned this CVE-2026-45321 with a CVSS score of 9.6 — critical.

If you’re a founder running a team that uses AI coding agents — Claude Code, Cursor, Copilot, Codex CLI, Windsurf, any of them — and one of those agents ran npm install against your repo during that six-minute window, here’s the uncomfortable question:

Can you prove it didn’t?

For most teams, the honest answer is no. They can’t. And that’s the bigger story.

What actually happened to TanStack

The attack chain is worth understanding in plain English, because it tells you why traditional defenses didn’t help. TanStack didn’t get phished. No maintainer leaked an npm token. Their 2FA was on. The official postmortem confirms it.

Instead, the attacker chained three weaknesses in TanStack’s GitHub Actions CI pipeline:

  1. A pull_request_target Pwn Request. When a fork opens a PR, GitHub normally runs the CI in the safe context of the fork — no secrets. But pull_request_target is a workflow trigger that runs in the base repository’s context with full secret access. TanStack used it for a legitimate-looking reason. The attacker submitted a PR that abused the trigger.
  2. Cache poisoning across the fork/base trust boundary. GitHub Actions caches dependencies between runs to speed builds up. The attacker found a path to write into the base repository’s cache from a fork build, planting a payload that would run on the next legitimate CI run.
  3. OIDC token extraction from the runner process. When GitHub’s CI runs, it gets a short-lived OIDC token it uses to authenticate to npm for publishing. The attacker’s code didn’t just steal the token from environment variables — it dumped it from the runner’s memory.

Once they had the OIDC token, they published. Two malicious versions of each package, roughly six minutes apart. The first version was a probe; the second contained the real payload. By the time TanStack noticed, the worm — TeamPCP calls it Mini Shai-Hulud — was already in the wild and spreading to other packages by harvesting tokens from anyone who ran npm install.

This is the supply-chain attack pattern that has been chewing through the JavaScript ecosystem for two years. TanStack is not the first. It is not even unusual. What’s unusual is how clean the attack chain is: the attacker didn’t compromise a human. They compromised the CI system that the humans trusted.

The affected packages and versions

GitHub’s advisory lists 42 compromised packages, 84 versions in total. A non-exhaustive selection from the advisory:

  • @tanstack/react-router1.169.5, 1.169.8 (patched: 1.169.9)
  • @tanstack/router-cli1.166.46, 1.166.49 (patched: 1.166.50)
  • @tanstack/vue-router1.169.5, 1.169.8 (patched: 1.169.9)
  • @tanstack/solid-start1.167.65, 1.167.68 (patched: 1.167.69)

The full list of 42 packages is in the GitHub advisory. The confirmed-clean families are @tanstack/query*, @tanstack/table*, @tanstack/form*, @tanstack/virtual*, @tanstack/store, and the @tanstack/start meta-package itself.

Detection signal: if you grep your installed node_modules/@tanstack/*/package.json files for the string "@tanstack/setup": "github:tanstack/router#79ac49eedf774dd4b0cfa308722bc463cfe5885c" in optionalDependencies — and you find it — you ran a poisoned version. Rotate everything. We’ll come back to this.

What the malware actually did

The payload was a ~2.3 MB obfuscated JavaScript bundle named router_init.js, pulled in via an optional dependency resolving to an orphan commit in a forked repository. Optional dependencies execute on install. They don’t need to be imported. They don’t need a runtime trigger. npm install is the entire kill chain.

Once running, the payload walked through every credential location a typical developer or CI environment has:

  • AWS Instance Metadata Service and AWS Secrets Manager
  • GCP metadata service (the one at 169.254.169.254)
  • Kubernetes service-account tokens (/var/run/secrets/kubernetes.io/serviceaccount/)
  • HashiCorp Vault tokens
  • ~/.npmrc for npm tokens
  • Environment variables (GITHUB_TOKEN, GH_TOKEN)
  • The gh CLI’s stored credentials
  • ~/.git-credentials
  • Every SSH private key under ~/.ssh/

Then it shipped the harvest to a set of Session/Oxen messenger endpoints — filev2.getsession.org and seed{1,2,3}.getsession.org. Session/Oxen are decentralized messaging networks. Hard to block at the firewall, hard to subpoena, hard to take down.

The blast radius for a developer machine is bad. The blast radius for a CI runner with cloud admin credentials is catastrophic. The blast radius for a developer machine that also runs an AI coding agent that has been delegated permission to npm install without asking — and which probably also has access to your .env, your AWS profile, and your cloud SDK config — is somewhere between “rotate everything you own” and “the company is over.”

Where AI coding agents make this worse

Here is the part most security writeups about TanStack are not going to cover, and the part that should make every founder reading this sit up.

Modern AI coding agents are autonomous. You ask Claude Code to “fix the failing test on the router branch.” It checks out the branch. It sees the lockfile is dirty. It runs npm install. It does this because you said yes to a sweeping permission prompt three weeks ago, or because you configured it to auto-approve common dev commands, or because it’s running in a --yolo-equivalent mode that everyone running these tools eventually defaults to once they get tired of approving the same five commands forty times a day.

The agent ran the command. The package executed. The credentials left your machine.

You will not see this in the agent’s chat output. The agent does not know it happened. The agent only knows the install succeeded. The malicious code did its work between lockfile resolve and script run — a phase the chat transcript does not surface. Your terminal scrollback is also unlikely to capture it, because the agent often runs in a child process whose stdout it summarizes, not echoes.

A week later, your AWS bill shows a coin miner running in a region you’ve never used. Or your GitHub Actions secrets start being used to publish a malicious version of your package. Or you’re the next entry in next quarter’s “supply-chain compromise of the week” newsletter.

You go to investigate. What do you actually have?

  • The agent’s chat transcript — partial, summarized, easy to overwrite, and on the agent vendor’s servers
  • Your shell history — only what you typed, not what the agent typed for you
  • The git log — shows the result, not the path the agent took
  • The package-lock.json — shows what got pinned, but you needed the lock before the install to know what was changed

What you do not have is the one thing you most need: a complete, append-only, local record of every shell command the agent actually ran on your machine, in order, with timestamps.

That’s the gap. And it is fixable today, for free, in about ten lines of Python.

What an agent command audit log gives you

Every Coograph install ships with a pre-tool-use hook at .claude/hooks/log-bash.py (and Codex CLI + OpenCode equivalents that write to the same files). Its entire job is one thing: append every bash command the agent is about to run to a per-project log under .coograph/. The files are gitignored. They live next to your code. Nothing leaves the machine. The agent doesn’t have to ask. The agent doesn’t even know the hook is there.

# .claude/hooks/log-bash.py — minimal shape
import json, sys, datetime, pathlib

event = json.load(sys.stdin)
if event.get("tool_name") == "Bash":
    cmd = event.get("tool_input", {}).get("command", "")
    log = pathlib.Path(".coograph/session.log")
    log.parent.mkdir(parents=True, exist_ok=True)
    with log.open("a", encoding="utf-8") as f:
        ts = datetime.datetime.utcnow().isoformat()
        f.write(f"{ts}\t{cmd}\n")

print(json.dumps({}))  # don't block the tool

That tiny file is the difference between “we got owned and have no idea what touched our credentials” and “we know npm install @tanstack/react-router@1.169.8 ran at 19:24:11 UTC on 11 May 2026 on Paul’s laptop, here are the credentials in scope, rotate them and move on.”

Audit logs are not glamorous. They do not prevent compromise. What they do is make the response to a compromise possible. Without them, every supply-chain headline becomes a fire drill across your whole company because you can’t scope the damage. With them, the fire drill turns into a fifteen-minute grep.

What a good agent audit log captures

If you build your own — or use Coograph’s — the design rules are short:

  1. Every command, no filter. The compromised one will not look interesting at the time. Filter at read time, not at write time.
  2. Append-only, plain text, local-first. A SQLite log is fine. A JSONL file is fine. A shipped-to-someone-else log is not fine, because the credentials in scope include the ones that could let an attacker delete your logs.
  3. Timestamps in UTC. When the next advisory arrives with a six-minute window, you need to match it to the second.
  4. Per-project, gitignored. You do not want a developer’s command history committed by accident. You also do not want one giant global log; per-project keeps the blast-radius math easy.
  5. The full command, not a summary. “Ran an install” is useless. npm install is barely better. npm install @tanstack/react-router is what you actually need.
  6. Independent of the agent vendor. If the log only exists in the agent’s UI, the agent vendor controls your incident response. That is the wrong direction.

Coograph’s log-bash.py does all six. We did not invent this idea. We are arguing it should be the floor, not a feature.

If you used @tanstack/* between 11 and 12 May 2026

Treat the next paragraph as the action list. Don’t read past it without doing the steps.

  1. Find the bad versions. In every project that depends on @tanstack/*, grep node_modules/@tanstack/*/package.json for the orphan-commit string 79ac49eedf774dd4b0cfa308722bc463cfe5885c. Also check your package lockfile history — git log -p package-lock.json — for any of the version numbers in the advisory. CI cache directories count.
  2. Rotate every credential the affected machine could see. AWS keys (including instance profile if you ran on EC2), GCP service-account keys, Kubernetes service-account tokens that machine could read, Vault tokens, GitHub PATs (including the gh CLI’s), npm tokens (~/.npmrc), and all SSH private keys in ~/.ssh/. Generate fresh ones. Revoke the old ones. Update everywhere that referenced them.
  3. Check your CI runner. Self-hosted runners are the highest-value target. If a runner pulled one of the bad versions, treat the entire runner as compromised, not just the project that triggered the install.
  4. Check published artifacts. If your CI publishes to npm, Docker Hub, PyPI, or anywhere else, audit recent publishes for anything that looks off. Worms spread by republishing through stolen tokens.
  5. Look for the lateral movement. Search your GitHub org’s audit log for unexpected workflow runs, new collaborators, new SSH keys added to accounts, or new personal access tokens in the days following 11 May.
  6. Then upgrade. Bump to the patched versions from the advisory. The patched versions are not enough on their own — the credentials are already gone — but you still need them.

If you do not have an agent command log on the affected machine, you cannot complete step 1 with confidence. That is the lesson.

The bigger pattern: trust boundaries inside your dev loop

TanStack’s compromise is one data point on a curve. Two years ago we worried about a single npm maintainer’s account being phished. One year ago we worried about typosquatted packages. This year the attackers are inside CI pipelines, replaying OIDC tokens, and weaponizing build caches across trust boundaries that nobody had drawn on a diagram.

Meanwhile, our development environment got an order of magnitude more powerful. AI coding agents now read, write, and execute on your behalf. They make hundreds of decisions per day that used to require a keystroke. Each of those decisions is a potential trust handoff: from you to the agent, from the agent to a shell command, from the shell command to a package, from the package to a remote server.

You cannot eliminate that surface. The productivity gain from AI coding agents is real. Going back to a world without them is uncompetitive. But you can — and at this point you must — make sure that every action the agent takes is observable on your terms, with your retention policy, on your hardware.

That’s why Coograph ships an agent command log by default, gitignored, local-first, on every project. We’re not selling you a feature. We’re stating a floor.

What we’re not saying

To be exact about the claim:

  • We are not saying TanStack was negligent. They had 2FA on. They had MFA-required publishing. Their CI used pull_request_target for a reason most teams using it use it for. Their postmortem is honest and useful.
  • We are not saying AI coding agents are uniquely insecure. They are uniquely capable, which makes the consequences of an underlying supply-chain compromise larger and faster.
  • We are not saying an audit log would have prevented this attack. Nothing on your developer machine would have prevented this attack. The log is what makes the cleanup possible.

What we are saying is simpler: if your team is running AI coding agents, and you do not have a local, append-only, per-project log of every shell command those agents ran, you are operating without the cheapest, most boring, most clearly-justified piece of incident-response telemetry available to you. Fix that this week.

Try it on your project

If you want the Coograph version of this, clone the repo next to your project and run the initializer from your AI tool’s chat:

# from your project root
git clone https://github.com/paullukic/coograph.git ../coograph

Then, inside your AI tool of choice:

  • Claude Code, Cursor, Copilot, OpenCode, Windsurf, Aider, Cline: type /coograph-init
  • Codex CLI: type $coograph-init (Codex reserves / for built-ins)

The initializer detects your stack, copies the bash audit hook into .claude/hooks/log-bash.py (plus .codex/hooks/log-bash.py and .opencode/plugin/log-bash.ts for Codex CLI + OpenCode users), wires up the slash-command workflow, and optionally builds the code graph. About two minutes. MIT-licensed. The logs live in your repo at .coograph/. We never see them.

Full walk-through at coograph.com/docs/getting-started/.

If you don’t want Coograph, port the ten-line Python hook above into your own .claude/hooks/ directory (Claude Code), or the equivalent hook surface for Cursor / Codex CLI / Windsurf. The point is not the tool. The point is the log.

Sources

Cut your AI coding bill 30–80%. Coograph is MIT-licensed and free forever. Pro is bespoke services.