Back bloom.security Slugfest Millions Of Ai Agents Download Exposed Npm Packages
AI agents are quietly turning people who’ve never installed a code package into new cogs in the supply-chain risk machine. They’re also supercharging the developers who were already part of it, pulling in far more than they ever used to manually.
We wanted to see exactly what that looks like in practice, so we studied how millions of AI agents behave across large enterprises. One number jumped out: the average agent installs 70 packages a month, with some developer endpoints hitting 350 a month.
So we asked: what does the shai-hulud look like? We found a way to take over thousands of packages downloaded by your agents daily - and the worst part - the gap was actively exploitable for 4 months after disclosure.
We identified more than 1,500 vulnerable packages with over 10 million downloads , and even caught and blocked customers’ AI agents attempting to download some. Only 4 months later, during which more packages became vulnerable, a fix was implemented.
Imagine this: an attacker compromises a single package. An AI agent on a popular package maintainer’s device autonomously pulls it for a project. That single endpoint is used to push malicious code to the popular package. All the package users are exposed. All because one person with the correct access was exposed.
The vulnerability exposes legitimate packages, and attackers compromise them. The AI agent does the distribution.
AI Agent as the New Users we Need to Defend
AI Agents constantly install software packages - but do they verify what those packages are? Just a few weeks ago, we caught a customer’s agent attempting to install one of the packages vulnerable to the issue we’ll describe below, and of course blocked it.
Supply chain attacks used to rely on a human actively installing a package. That’s no longer the main way packages reach an endpoint. Coding agents like Claude Code, Codex, Gemini and Cursor now pick, install and run dependencies on their own, often without a human-in-the-loop, and usually without anyone auditing the package, its maintainer or its release history.
We measured this across dozens of organizations:
Each agent brings in new packages at a rate of 0.3-1.2 per day , depending on the agent type. On developer machines, that rises to 2-3 per day .
Each agent brings in new packages at a rate of 0.3-1.2 per day , depending on the agent type. On developer machines, that rises to 2-3 per day .
The average endpoint runs 3 agents (CLIs, IDEs, extensions and assistants). Developer machines run 5 or more .
The average endpoint runs 3 agents (CLIs, IDEs, extensions and assistants). Developer machines run 5 or more .
So a typical endpoint picks up 70 agent-installed packages a month . A developer machine picks up close to 350 . These are the direct installs only and don’t count the dependencies each one brings in.
So a typical endpoint picks up 70 agent-installed packages a month . A developer machine picks up close to 350 . These are the direct installs only and don’t count the dependencies each one brings in.
Every one of those installs is a chance for a postinstall hook to run. A hijacked package doesn’t need to fool a developer anymore. It only needs to be the package an agent chooses.
The Flaw in One Paragraph
Npm’s trusted publishing lets a package say: “only accept new releases built by this GitHub repository”. The problem is how it identifies that repository. It records the repository’s address, such as bloom/repository, instead of the permanent ID GitHub assigns it, at least until a version is first published from that repository. It is like a lock that checks the address on the door rather than who owns the house: when the owner moves out, whoever moves in gets the key. On GitHub, that owner/repo address is called a slug , and abandoned slugs can be claimed by anyone. SlugFest is what happens when they are.
“A name is not an identity. npm’s trusted publishing treats the two as the same thing, and anyone who picks up an abandoned name inherits the trust that came with it.”
How Trusted Publishing Works, and Where It Breaks
For years, npm maintainers handled publishing secrets the hard way. A long-lived automation token sat somewhere in the CI, was rotated rarely if ever, and was shared by every workflow that needed to publish. Leaked tokens have been one of the most persistent causes of npm compromises.
Trusted Publishing was meant to replace all of that. Instead of a static secret, a GitHub Actions workflow requests a short-lived OIDC token, exchanges it at the npm registry for a publish credential, and the credential expires within minutes. No more tokens leaking in build logs, and no more credential sprawl. PyPI has run on this model for years, and it works. npm made the feature generally available in July 2025, and adoption is still estimated at under 20% of active packages.
The gap is in what npm stores. When a maintainer runs npm trust github with the --allow-publish flag, the CLI sends the registry three fields:
repository: the owner/repo string (e.g. bloom/repository)
repository: the owner/repo string (e.g. bloom/repository)
workflow_ref.file: the file name of the workflow YAML (e.g. release.yml)
workflow_ref.file: the file name of the workflow YAML (e.g. release.yml)
environment: an optional GitHub Actions environment name
environment: an optional GitHub Actions environment name
That is the entire binding. GitHub’s OIDC tokens also carry two identifiers that npm ignores completely:
repository_id: a number that identifies the repository and never changes, even through renames and transfers
repository_id: a number that identifies the repository and never changes, even through renames and transfers
repository_owner_id: a number that identifies the owner, and also never changes
repository_owner_id: a number that identifies the owner, and also never changes
The CLI never sends them, and the registry only stores them after the first successful publish from that repository.
At publish time, the workflow requests an OIDC token from GitHub, sends it to npm’s exchange endpoint, and receives a short-lived publish credential. The exchange succeeds if, and only if, the token’s repository claim matches the stored slug and the workflow file name matches the stored one. The numeric IDs are in the token, but until that first publish, npm has nothing to check them against.
The Attack: Zero Clicks, Zero Credentials, Millions Affected
The precondition is simple: the victim’s Trusted Publisher points to a GitHub slug the attacker can claim, and the trust repository was never used to publish a version of the package - once a publish event happens, the immutable repository id is recorded. This happens whenever a maintainer renames, transfers, or deletes the bound repository - or when the bound owner account is renamed or deleted. In each case, the original owner/repo path becomes claimable.
Once the slug is free, the attack is mechanical:
Create a repository at the freed slug. GitHub issues a fresh repository_id - different from the one the legitimate maintainer originally controlled.
Create a repository at the freed slug. GitHub issues a fresh repository_id - different from the one the legitimate maintainer originally controlled.
Add a workflow file whose name matches the bound basename (e.g. release.yml). The workflow requests an OIDC token with the standard id-token: write permission.
Add a workflow file whose name matches the bound basename (e.g. release.yml). The workflow requests an OIDC token with the standard id-token: write permission.
Trigger the workflow. The minted OIDC token carries the reclaimed slug in its repository claim and the new numeric ID in its repository_id claim.
Trigger the workflow. The minted OIDC token carries the reclaimed slug in its repository claim and the new numeric ID in its repository_id claim.
Submit the token to the npm exchange endpoint. The registry sees a repository claim that matches the stored slug, a workflow filename that matches the stored basename, and issues a publish credential.
Submit the token to the npm exchange endpoint. The registry sees a repository claim that matches the stored slug, a workflow filename that matches the stored basename, and issues a publish credential.
No credential belonging to the legitimate maintainer is touched. No npm account is compromised. No GitHub account is compromised. The attacker receives a valid, time-limited publish token for a package they do not own.
We ran this end-to-end against an npm scope we own and researcher-controlled GitHub repositories. We were able to publish to the npm package from a second GitHub repository belonging to a different organization. The only thing the two had in common was the slug. It originally belonged to the repository the package trusted, and was claimed by the second once it was freed.
Impact: From Slug Reclaim to Supply Chain Attack
A publish token is the end of the story only on paper. In practice, it is the beginning. An attacker who successfully exchanges the OIDC token can:
Publish a new version of the victim package under the legitimate package name, inheriting all existing reputation, download counts, and dependent relationships.
Publish a new version of the victim package under the legitimate package name, inheriting all existing reputation, download counts, and dependent relationships.
Get installed by AI agents. Agents resolve dependencies on their own, tend to take the latest version, and rarely check where a package came from. A malicious version published through a reclaimed slug gets pulled in and runs with no human-in-the-loop.
Get installed by AI agents. Agents resolve dependencies on their own, tend to take the latest version, and rarely check where a package came from. A malicious version published through a reclaimed slug gets pulled in and runs with no human-in-the-loop.
Include a malicious postinstall hook. Every developer and CI system that runs npm install after the malicious publish executes attacker-controlled code immediately, without any download interception or signature check required on their part.
Include a malicious postinstall hook. Every developer and CI system that runs npm install after the malicious publish executes attacker-controlled code immediately, without any download interception or signature check required on their part.
Target the transitive dependency cone. A single popular package can have tens of millions of weekly downloads. Recent npm compromises have demonstrated the blast radius of this scale in practice.
Target the transitive dependency cone. A single popular package can have tens of millions of weekly downloads. Recent npm compromises have demonstrated the blast radius of this scale in practice.
Produce a provenance attestation. The attacker’s publish is signed by Sigstore against the attacker’s repository. Downstream tooling that checks only whether a package is signed - rather than verifying the attestation source matches the maintainer’s expected repository - will surface the attestation as valid.
Produce a provenance attestation. The attacker’s publish is signed by Sigstore against the attacker’s repository. Downstream tooling that checks only whether a package is signed - rather than verifying the attestation source matches the maintainer’s expected repository - will surface the attestation as valid.
The attack requires no insider knowledge, no credential theft, and no race condition. The only precondition is slug availability (on a binding that has not yet been used to publish), which is entirely a function of the victim’s own repository hygiene.
How Widespread Is This? We Scanned the Entire Registry
To understand the in-the-wild exposure, we built a scanner that tests the ability to perform the attack end-to-end - we started by checking which packages have trusted publishing configured. We then checked the repositories and their owners to validate which are claimable.
We found over 1500 packages vulnerable, with a combined monthly download tally higher than a million, and a total of over 10 million downloads. One thing to keep in mind is that this number is relative not to the total number of packages on npm, but rather the total number of packages with trusted publishing configured, which is much lower - meaning this issue was widespread. We are not releasing a list of affected packages as the issue is still prevalent.
The feature was introduced less than a year ago at the time of writing, and while still gaining traction, is far from global adoption - meaning this issue had the potential to grow exponentially.
SlugFest by the numbers
1,500+ vulnerable npm packages
1,500+ vulnerable npm packages
10M+ total downloads; 1M+ per month
10M+ total downloads; 1M+ per month
~70 agent-installed packages per endpoint per month (~350 on developer machines)
~70 agent-installed packages per endpoint per month (~350 on developer machines)
0 credentials needed to take over a package
0 credentials needed to take over a package
Over 120 days from disclosure to fix
Over 120 days from disclosure to fix
This is not a fundamental design flaw in OIDC-based publishing. It is an omission that has a clean, low-cost remediation.
At trust-creation time, owner/repo should be resolved to its current GitHub repository_id and repository_owner_id via the GitHub REST API (GET /repos/{owner}/{repo}). Both values should be stored alongside the slug. At exchange time, require exact equality between the OIDC token’s repository_id claim and the stored value. Reject mismatches.
PyPI’s Warehouse already implements precisely this pattern, protecting PyPI publishers against slug-reclaim attacks since the feature launched. The npm implementation of the trust subsystem only recorded the immutable IDs after a binding’s first publish, rather than at trust-creation time.
The fix npm decided on differs slightly - they still create the binding at first publish time, however, if no publish event was recorded within the first 48 hours after the trust binding, it is invalidated.
We reported the issue to the security team on June 6, 2026, through the GitHub HackerOne program. The issue was triaged and validated by the team on June 9, 2026. The last response from the team came on July 8, 2026, stating they are “working on fixing the issue and determining its risk and severity”. We have since sent 6 enquiries, including a mediation request to Hackerone (who manage the program), all of which have gone unanswered. We believe npm users should be aware of the security issues in the platform they use, and as such, chose to release this - in a hope that it will apply pressure and expedite the remediation process. Before publishing, we sent a copy of the blog to the npm team through hackerone, and ran our test again for verification, which is how we discovered a fix had been implemented. The team replied to our final enquiry apologizing for the lack of communication, thanking us for the effort and informing us a fix had indeed been implemented, on the 2.10 (details here ). While we are glad a fix is in place, we have still not heard back from the npm team - responsible disclosure deserves better. Notice that while the blog discussed GitHub, the issue was prevalent in npm’s GitLab trusted publishing implementation as well.
If you maintain npm packages
Audit your Trusted Publisher bindings. Run npm trust list for each of your packages and verify that the listed repository slug still resolves to the repository you control.
Audit your Trusted Publisher bindings. Run npm trust list for each of your packages and verify that the listed repository slug still resolves to the repository you control.
If you have renamed or transferred a bound repository, delete the existing trust binding and re-create it pointing at the current slug.
If you have renamed or transferred a bound repository, delete the existing trust binding and re-create it pointing at the current slug.
Treat a repository deletion, transfer or rename as a revocation event - if you delete a bound repo and do not intend to recreate at that slug, remove the trust binding first.
Treat a repository deletion, transfer or rename as a revocation event - if you delete a bound repo and do not intend to recreate at that slug, remove the trust binding first.
Monitor for unexpected publish events. A publish you did not initiate from the expected repository is evidence of exploitation.
Monitor for unexpected publish events. A publish you did not initiate from the expected repository is evidence of exploitation.
For future trust bindings, make sure to publish a version using the binding immediately after its creation, to leave no room for exploitation.
For future trust bindings, make sure to publish a version using the binding immediately after its creation, to leave no room for exploitation.
Audit the software packages installed in your environment, but don’t stop there - audit all of the installed software - extensions, MCPs, skills, applications, plugins, repositories etc. These kinds of issues are not limited to software packages - just recently we disclosed a similar issue for extensions .
Audit the software packages installed in your environment, but don’t stop there - audit all of the installed software - extensions, MCPs, skills, applications, plugins, repositories etc. These kinds of issues are not limited to software packages - just recently we disclosed a similar issue for extensions .
Enforce a supply-chain firewall to gatekeep what gets installed in your environment.
Enforce a supply-chain firewall to gatekeep what gets installed in your environment.
Enforce AI agent usage policies to allow your user to utilize them in a secure manner, which allows productivity without putting your organization at risk.
Enforce AI agent usage policies to allow your user to utilize them in a secure manner, which allows productivity without putting your organization at risk.
A Note on the Ecosystem
npm Trusted Publishing reached general availability in July 2025. The feature addresses a real and longstanding problem - static automation tokens are a persistent source of credential theft in open source supply chains. The architecture is correct. The omission is fixable.
What this research demonstrates is something the security community has seen repeatedly across supply chain attacks: the binding between a human-readable identifier and the thing it identifies is the most important invariant in any trust system. When that binding can be silently replaced - by the ordinary act of renaming a repository, not by any attacker action - the trust guarantee collapses.
Every push you make, every version you take - make sure npm knows which repository actually made it.
The full story
This article is one source in a clustered incident — the cluster page carries the summary, timeline and every other outlet covering it.
