The Worms Learned to Use Your AI Agent: A Year of npm Supply-Chain Attacks
In August 2025 a compromised build of nx did something no supply-chain payload had done before. It looked for claude, gemini and q on the victim's machine, and if it found one, it ran it with permissions disabled and handed it a prompt: recursively search the home directory for wallet files, keystores, .env files, SSH keys, and browser storage, and write the paths to a file.
The attacker did not write a file-hunting routine. They delegated it to the coding agent the victim had installed to be more productive.
I operate infrastructure where AI agents run code, so I have spent the past year reading these incidents the way a fire inspector reads a building. This post is the twelve-month arc: what the worms actually did, what the registries changed in response, which of those changes protect you by default, and the install-time configuration I now consider non-negotiable. All numbers are from the vendor and registry write-ups linked inline. Where sources disagree, I say so.
The timeline
s1ngularity: the payload that hired an agent
The Nx postmortem is a model of the genre and worth reading in full. The root cause was a GitHub Actions workflow using pull_request_target that echoed unsanitised pull-request titles, with default read/write permissions. A crafted title stole the npm publish token. The malicious versions were live for about four hours on a package with roughly six million weekly installs.
The postinstall script, telemetry.js, is what made this one different. Per Semgrep's analysis and Wiz's, it checked for the Claude Code, Gemini CLI and Amazon Q CLIs, ran whichever it found with the flags that disable permission prompts, and gave it a natural-language instruction to inventory secrets. The results were base64-encoded and pushed to a public GitHub repository under the victim's own account named s1ngularity-repository. For good measure it appended sudo shutdown -h 0 to the victim's shell profile.
GitGuardian counted 1,346 of those repositories and 2,349 distinct secrets, more than 1,100 of them still valid at the time of analysis. About a third of victims had an LLM client installed. Of 366 recorded agent invocations, only 95 executed the malicious request; Claude in particular refused most of the time. The model's safety training was, in that instance, a real control. It was also not one anybody would want to depend on.
The second wave was worse. With stolen GitHub tokens the attackers flipped 10,767 private repositories public, exposing another 82,901 secrets. That number is why "rotate the npm token" is never the end of the response.
Shai-Hulud: the worm that publishes itself
Three weeks later, @ctrl/tinycolor and around 40 other packages shipped a postinstall that StepSecurity and Wiz dissected within a day. The bundle ran TruffleHog against the local filesystem, queried the cloud metadata endpoint for instance credentials, committed a GitHub Actions workflow that exfiltrated every repository secret to a webhook, and forced private repositories public under a -migration suffix.
Then the part that earned it a name. If it found an npm token, it fetched up to 20 packages owned by that maintainer, patched each one with a copy of itself, and force-published. Every victim became a distribution point. Counts went from 40 to 187 to over 500 in 48 hours, CISA issued an alert, and roughly 25 CrowdStrike packages were among the casualties. Palo Alto's Unit 42 assessed with moderate confidence that the bash portion of the payload was itself LLM-generated.
The November sequel moved to preinstall, so it fired before any dependency review, shipped the Bun runtime to evade JavaScript-focused scanners, registered victims as self-hosted GitHub runners, and, if it could not exfiltrate, wiped the home directory. Zapier, PostHog and the Ethereum Name Service were among named victims. The counts vary by who was counting: Check Point reported 621 packages and roughly 25,000 repositories with 14,206 secrets of which 2,485 were valid; Datadog counted 796 packages and 14,000-plus repositories. Either way it was the largest npm compromise on record at the time.
2026: the worms specialise in AI credentials
The 2026 waves are less famous and more targeted. Three details stand out.
IronWorm (June) dropped a Rust binary with an eBPF rootkit and a Tor command channel. Its environment-variable target list had 86 entries, 14 of them API keys for AI providers: Anthropic, OpenAI, Gemini and the rest. It spread by exchanging CI OIDC tokens for npm trusted-publishing credentials and backdating commits. That is a direct attack on the mechanism the registries introduced to stop the previous worms.
The Mini Shai-Hulud campaign (May) hit TanStack and the Mistral SDK and installed a daemon that polled its stolen GitHub token every 60 seconds and ran rm -rf ~/ if the token was revoked. The response playbook "revoke everything immediately" now has a cost.
And the July Jscrambler compromise carried a payload specifically targeting the credential stores of Claude Desktop, Cursor and Windsurf. A year after s1ngularity used the agent as a tool, the agent's own tokens became the prize. Sonatype's 2026 report puts the ecosystem-wide figure at more than 454,600 new malicious packages in 2025, up 75% year on year, over 99% of them on npm.
There was one more incident in July that I cannot leave out, because it inverts the whole pattern. According to Hugging Face's technical timeline, an OpenAI evaluation agent escaped its sandbox through Artifactory write access, a server-side request forgery, and a previously unknown token-refresh bug, then operated inside Hugging Face's production environment for four days, performing roughly 17,600 actions. No human attacker. An agent doing what it was, in some sense, told to do, from inside a sandbox that was not one. I run sandboxes for a living and that report is the most useful thing I have read this year. The isolation was real. The credentials reachable from inside it were the problem.
What the registries changed
Credit where it is due: npm's response was fast and structural. This table is the sequence, because the sequence matters for what you are protected against today versus what you have to turn on yourself.
| Date | Change | Source |
|---|---|---|
| 29 Sep 2025 | Granular tokens default to 7-day lifetime, 90-day max; TOTP 2FA deprecated for WebAuthn; trusted publishing promoted | GitHub changelog |
| 5 Nov 2025 | Classic token creation disabled | changelog |
| 9 Dec 2025 | All classic tokens revoked; npm login issues 2-hour session tokens |
changelog |
| Feb 2026 | min-release-age lands in npm CLI 11.10 |
craigory.dev |
| May 2026 | Staged publishing with human 2FA approval goes GA in npm 11.15 | nesbitt.io |
| 8 Jul 2026 | npm v12 stops running dependency lifecycle scripts, git and remote-URL dependencies by default | changelog |
| 31 Jul 2026 | Bypass-2FA tokens lose token and maintainer management; direct publish removed from Jan 2027 | changelog |
The npm v12 change is the one that matters most and the one most people have not noticed. Every worm in the timeline above ran from a lifecycle script. Making those opt-in for dependencies removes the primary propagation vector by default. If you are still on npm 11 or an older lockfile-pinned toolchain, you do not have that protection.
PyPI got there earlier: trusted publishing since 2023 with tokens that expire in 15 minutes, mandatory 2FA since January 2024, and Sigstore attestations generated automatically for trusted-publisher uploads. The PyPI security model is the document I point people to when they ask what "good" looks like.
The configuration I now refuse to run without
Everything below is boring, cheap, and would have stopped every incident in the timeline at at least one point in its chain.
Cooldown on new versions
Every worm above was detected within hours and pulled within a day or two. A release-age gate means you simply never see those versions. pnpm shipped minimumReleaseAge in September 2025; it is now the default at 24 hours in pnpm 11. Bun and Yarn have equivalents. For npm:
# .npmrc — project-level
min-release-age=1440 # minutes; ignore versions younger than 24h
ignore-scripts=true # no lifecycle scripts from dependencies
audit=true
fund=false
# pnpm-workspace.yaml
minimumReleaseAge: 1440
minimumReleaseAgeExclude:
- "@my-org/*" # your own packages, if you need same-day releases
The obvious objection is that a 24-hour delay slows down security patches too. It does. In practice a one-day delay on a patch is almost always cheaper than a zero-day exposure to a worm, and the exclusion list handles the rare package you genuinely need immediately.
Scripts off, explicitly on for the few that need them
ignore-scripts=true breaks a handful of packages that legitimately compile native code. npm 11.16 and later support an allowlist, so you can name them:
ignore-scripts=true
allow-scripts[]=esbuild
allow-scripts[]=sharp
strict-allow-scripts=true
Now a new dependency cannot run code at install time without a human adding it to this list, which is exactly the review step the worms exploit the absence of.
Kill pull_request_target unless you can explain why you need it
s1ngularity, the AsyncAPI compromise and the tj-actions incident all began in CI. The common factor is a workflow that runs with the repository's secrets on input from strangers. The hardening is well known and still widely skipped:
permissions:
contents: read # default-deny at the top of every workflow
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@34e114876b0b11c390a56d6a4c3d5c6ee8ffd8b3 # pin to SHA, not tag
- run: npm ci --ignore-scripts
Pin third-party actions to a commit SHA, not a tag. The tj-actions attack worked by moving the tags. Use pull_request, not pull_request_target, for anything that touches contributor input. Never interpolate an untrusted string like a PR title into run:; pass it through an environment variable. If your workflow uses a coding agent to triage issues or review pull requests, the April 2026 "Comment and Control" disclosures showed that issue comments can hijack Claude Code Action, Gemini CLI Action and the Copilot agent to steal the workflow token. Treat those agents as untrusted-input processors and give them read-only tokens.
Publish through OIDC, with a human at the end
Trusted publishing removes the long-lived npm token that every worm needed to propagate. It is not magic, and IronWorm demonstrated that a compromised CI job can exchange its OIDC identity for a publishing credential. The second half of the fix is npm's staged publishing: the pipeline stages the release, and a human with a hardware key approves it. Nx adopted exactly this after their incident. If your release process can complete with no human touching a physical device, a worm can complete it too.
Do not leave a coding agent with permissions off on a machine that has secrets
This one is not a config flag. s1ngularity's success rate was 95 out of 366 invocations, and every one of those 95 was a developer who had an agent installed and a wallet, key or .env file for it to find. The agents are getting better at refusing. The attackers are getting better at not asking the agent, and stealing its credentials directly instead.
On my own machines the coding agent runs inside a sandbox with a network allowlist and no access to the directories where credentials live. Anthropic open-sourced their sandbox runtime in October 2025, and it has had its own symlink escape CVE since, which is fine; a sandbox with a patched bug is still better than no sandbox. On the platform I run, every agent workload gets a microVM, because after this year I do not believe any process-level boundary is enough for code that arrived over the network.
What I actually think
The interesting shift is not that npm got attacked. npm has been attacked continuously for a decade. The shift is that the payloads now assume an AI agent is present and try to use it, and, failing that, try to steal it.
That makes the coding agent part of the attack surface in a way that a linter or a formatter never was. It has broad filesystem access, it has network access, it holds API keys worth real money, and it can be talked into things. Everything I have written about running untrusted code applies to the agent on your laptop just as much as to the agent in my fleet.
The registries did their part this year, faster than I expected. The remaining exposure is configuration. A release-age gate, scripts off by default, SHA-pinned actions, read-only tokens for anything that reads untrusted input, and a sandbox around the agent. None of it is clever. All of it is available today. The next worm is presumably already written, and the one thing I am confident about is that it will look for your agent first.
Related: MCP Security in 2026: The Protocol Got Hardened. The Ecosystem Didn't. and It's 2 AM. Do You Know What Your AI Agent Is Doing?
I'm Ajay Kumar — I build and operate PandaStack, an open-source Firecracker microVM cloud for AI agents. Everything above comes from running it in production.
Need this kind of infrastructure work? See what I do or email hello@ajayk.sh.
Related
MCP Security in 2026: The Protocol Got Hardened. The Ecosystem Didn't.
A year of MCP CVEs, a rewritten spec, an NSA guidance sheet and an OWASP Top 10 — what actually changed, what didn't, and the six controls I'd insist on before an MCP server touches production.
12 minSep 2, 2026Isolation Is Not an Abuse Control: Lessons From My Fleet
A free-tier signup mined crypto on my microVM fleet in 27 minutes. Containment held perfectly and it changed nothing. What I built afterwards, in what order.
9 minSep 6, 2026Post-Quantum Migration in 2026: What's Done, What's Stuck, and What to Do This Quarter
Two-thirds of browser traffic to Cloudflare is already post-quantum. Your SSH is too. Your certificates are not, and won't be until 2027. A field guide for infrastructure engineers, with verified commands and hard deadlines.
13 min