npm SlugFest Explained: How Trusted Publishing Let Attackers Hijack 1,500 Packages
A naming flaw in npm Trusted Publishing exposed more than 1,500 packages to takeover with no stolen credentials. Here is how SlugFest worked and what to audit now.
On this page 10 sections
SlugFest is a flaw in npm Trusted Publishing that let an attacker publish new versions of a package they did not own. The attacker only had to claim an abandoned GitHub repository name. No password or token theft was needed. Bloom Security counted more than 1,500 exposed packages before GitHub shipped a fix in October 2026.
This explainer is for maintainers who publish to npm from GitHub Actions, and for the engineers who depend on those packages. It covers how the SlugFest flaw worked, what GitHub changed, and the short audit worth running this week. Backtrace covers supply chain security for working developers, so the focus is on what you can check today.
What Trusted Publishing Is Supposed to Do
Trusted Publishing replaces long-lived npm tokens with short-lived credentials. A CI workflow proves its identity with an OpenID Connect (OIDC) token, which is a signed statement from GitHub about where a job is running. npm checks that statement and hands back a temporary publish credential.
The idea is sound. A stolen token is the classic way a package gets hijacked, and a token that expires in minutes is much harder to steal and reuse. Many maintainers moved to it after the string of worm attacks earlier this year.
The weak point was the binding. A maintainer tells npm which repository and workflow may publish a given package. If npm identifies that repository loosely, the whole trust model rests on a name.
How SlugFest Worked
According to Bloom Security's SlugFest research, npm tied a package to its GitHub repository by the owner and repo name, called the slug. GitHub also gives every repository a permanent numeric ID. npm only stored that ID after the first successful publish.
That gap created a window. Suppose a maintainer configured a binding, then renamed the repo, transferred it, or deleted the account before any version shipped from it. The old slug became free for anyone to register.
The attack then needed four steps.
- Find a package whose bound repository no longer exists under that name.
- Create a new repository at the freed slug.
- Add a workflow file with the same filename the binding expects, such as release.yml.
- Run it with the id-token write permission and receive a valid publish credential.
The OIDC token carried the reclaimed slug, so npm's exchange endpoint accepted it. The real maintainer's accounts were never touched. The attacker's build might even carry a valid Sigstore attestation, which means a tool that only checks for a signature could be fooled.
How Big Was the Exposure?
Bloom reported the problem to GitHub on June 6, 2026, and found more than 1,500 vulnerable packages. Those packages were downloaded over 10 million times in total, and more than 1 million times a month. The scope is narrower than the whole registry, since only packages with Trusted Publishing configured were at risk.
The reach grows when you add automation. Bloom estimates that an average endpoint installs about 70 agent-selected packages a month, and a developer machine about 350. Coding agents tend to grab the latest version without looking at where it came from. The Hacker News reported that the flaw stayed exploitable for roughly four months before the fix.
Backtrace editors checked the timeline against both sources. Bloom says its last reply from GitHub came on July 8, and that the fix landed on October 2 without a notice to the researchers. That is a long window for a registry that sits under most JavaScript projects.
What Did GitHub Change?
GitHub's changelog entry from October 2 describes the fix. It does not switch npm to numeric IDs. It shortens the window instead.
Here is what changed, in plain terms:
| Change | What it means |
|---|---|
| 48-hour expiry | A binding with no successful publish stops working after 48 hours. |
| Validation | The first successful publish marks a binding as validated and exempt from expiry. |
| Identity changes | Changing the repository or project starts a new trust relationship and a fresh 48 hours. |
| issue_comment events | npm now rejects Trusted Publishing tokens from issue_comment triggers. |
| Existing restriction | The earlier block on pull_request_target tokens still applies. |
The result is a smaller attack window. An abandoned binding can no longer sit dormant for months waiting for someone to claim the slug. Bloom notes that PyPI already resolves the numeric repository ID at the time a binding is created, which closes the hole at the source.
Is My Package Still at Risk After the Fix?
Possibly, in a narrow case. The fix expires unvalidated bindings, so a binding that never published anything will stop working after 48 hours. A validated binding that points at a slug you later abandon is a different story. Treat renames, transfers and deletions as events that require cleaning up the binding.
A Short Audit for Maintainers
Start with the list of bindings. Bloom recommends running npm trust list for each package you maintain and confirming that every slug still points to a repository you control. It takes a few minutes per package.
Then work through these steps in order:
- Delete any binding whose repository was renamed or transferred, and recreate it against the current slug.
- Remove the binding before you delete a repository you will not bring back.
- Publish a version right after creating a new binding, so it becomes validated.
- Search your workflows for issue_comment triggers and move publishing to push, release or workflow_dispatch.
- Watch for publish events you did not start.
Do not skip packages you consider finished. Dormant packages are the likeliest to have a stale binding, because nobody has published or renamed anything in a while. A small utility with ten thousand weekly downloads and no recent commits is exactly the profile an attacker would look for.
The last item matters most for packages with many dependents. A quick alert on unexpected releases buys you hours, and hours are what limit the damage in a supply chain incident.
Why the issue_comment Change Matters
The second half of the fix is easy to overlook. GitHub Actions can run a workflow when someone comments on an issue. If that workflow also holds publish rights, a comment from a stranger can become a trigger for a release. npm already blocked tokens from pull_request_target events. It now blocks issue_comment events too.
Check how your release workflow starts. Publishing from a push to a protected branch, a tagged release or a manual dispatch is easier to reason about. Each of those needs someone with write access to act first. A comment does not.
One more detail is worth knowing. Expired bindings stay visible in the trusted publisher settings, but they do not count against the per-package limit. You can recreate one at any time, which starts a new 48-hour window. Plan to publish within that window, or the binding lapses again.
What Dependents and Security Leads Should Do
If you only consume packages, you cannot fix the binding. You can slow the spread. A short delay before adopting brand-new versions gives scanners time to flag a bad release. Many teams now set a minimum release age in their package manager for exactly this reason.
Bloom also advises auditing everything agents can install, including extensions, MCP servers and plugins. Its earlier Extension Resurrection research found 87 packs on Open VSX and 677 on the VS Code Marketplace that pointed at extensions that did not exist yet. The pattern is the same as SlugFest: a name that nobody owns.
Our guide to how software supply chain attacks work covers the broader playbook. Agent-driven installs add their own risk, which we cover in the piece on hallucinated dependencies from AI coding tools. If a package in your tree was already hit, the steps in our Shai-Hulud response checklist still apply.
The Bigger Lesson About Names
SlugFest is not really about npm. It is about trusting a name that someone else can later own. Package names, repository slugs, extension names and even internal service names all share the problem. Any time identity rests on a label, ask what happens when the label is freed.
Organizations can apply the same test to their own internal tooling. Look at deploy keys, cloud trust policies and CI roles that name a repository. A policy that trusts "any workflow in org/repo" is only as safe as that repository name staying yours. When a team renames a repo or retires a project, the cleanup list should include every trust relationship that mentioned it.
Numeric IDs, signed provenance and short expiry windows all help. None of them replace the habit of cleaning up after a rename. A binding is a permission, and permissions should not outlive the thing they were granted for.
Conclusion
SlugFest showed that a convenient shortcut in npm Trusted Publishing could turn an old repository name into a publish key. GitHub's 48-hour expiry closes the worst of it, but the cleanup is still yours. Run npm trust list today, delete stale bindings, and publish once after creating each new one. Then set up an alert for releases you did not start.