# npm Stored the GitHub Slug. The Id Waited for a Publish.

> The primary is Golan Myers's SlugFest post at Bloom Security, 13 October 2026. npm trusted publishing stored a GitHub slug and matched it until the first publish. Bloom published from a second organization onto a package it controlled. The 2 October changelog expires unvalidated configurations after 48 hours. Bloom did not publish the package list.

Source: https://hackerlogs.com/blog/npm-slug-trust-48h
Published: 2026-10-13

## Key takeaways

- The primary is Golan Myers at Bloom Security, 13 October. A trusted publisher stored the GitHub owner/repo slug. The permanent repository id was recorded at the first publish, not when the binding was created.
- Bloom published from a second organization onto a package and repositories it controlled. The shared fact was the freed slug. No maintainer credential was used in that test.
- The GitHub changelog on 2 October expires unvalidated configurations 48 hours after creation. A first successful publish validates the binding. GitHub's statement does not say older publishes were rewritten.
- Bloom's scan says more than 1,500 packages and more than 10 million downloads. They withheld the names. The scan section still calls the issue prevalent. Do not treat 1,500 as tonight's open set.
- A provenance attestation from this path can be valid and still name the other repository. Checking that a signature exists is not the same check as reading which repository signed.
- From the 6 June report to the 2 October changelog is 118 days. Bloom's numbers box says over 120. Use the dates in the post.

[Golan Myers's SlugFest post](https://bloom.security/blog/slugfest-millions-of-ai-agents-download-exposed-npm-packages) at Bloom Security, 13 October 2026, says npm trusted publishing stored a GitHub owner/repo slug and matched that string until the first publish from the repository. Bloom then published a version of a package it owns from a second organization's repository. The slug was the thing the two repositories shared. No maintainer credential was required for that test.

Hacker News tonight led with an OpenAI conference-room arrest, the Gemini 4 Argon launch, and a Ledger hardware implant. US X trends at 22:36 UTC were football, a film, and that launch. Reuters in this window had no new operator control failure. The registry fact is Bloom's post. The changelog it points at is from 2 October.

 High because a repository that did not originally own the binding obtained a publish credential for a package name, in a test Bloom ran on infrastructure it controlled. It is not a count of malicious versions on npm. Myers told Help Net Security that wild exploitation is unknown.

<img src="/npm-slug-trust-48h.svg" alt="Three panels for npm trusted publishing: a slug match until the first publish, a 48-hour expiry on unvalidated configs, and bindings a maintainer still has to recheck." width="1200" height="675" />

## What the binding stored.

The [npm trust docs](https://docs.npmjs.com/cli/v12/commands/npm-trust/) for `npm trust github` take `--repository` as owner/repo, `--file` as the workflow filename, and an optional environment. They do not take a repository id. `npm trust gitlab` takes a project path of the form group/project. `npm trust circleci` takes organization, project, and pipeline definition UUIDs. The GitHub and GitLab forms are names. The CircleCI form is already ids.

GitHub's OpenID Connect tokens carry `repository`, `repository_id`, and `repository_owner_id`. The numeric claims survive a rename or a transfer. Myers says npm ignored them until the first successful publish from that repository. Until then, the exchange matched the slug and the workflow filename. This brief does not repeat the steps Bloom used to show that.

PyPI's [Warehouse internals](https://github.com/pypi/warehouse/blob/main/docs/user/trusted-publishers/internals.md) say that, for GitHub, PyPI looks up the owner's numeric id when the trusted publisher is configured and checks `repository_owner_id` when it mints a token. That is an owner-id check at configuration time. It is the contrast on the page this brief actually read. Bloom says Warehouse stores the repository id as well. This read did not go past the owner-id section.

> The [changelog](https://github.blog/changelog/2026-10-02-unvalidated-npm-trusted-publishing-configurations-now-expire/) says unvalidated configurations expire 48 hours after creation and can no longer authorize publishing. A configuration becomes validated, and exempt from expiry, after its first successful publish. Changing the repository or project identity requires a new trust relationship. Ordinary edits do not restart the deadline. npm's docs say that first publish binds the configuration to the repository's immutable identity. GitHub's statement to Help Net Security repeats the expiry sentence. It does not say a publish from before the change was rewritten. Bloom's description of the fix matches the narrower reading: the id is still recorded at first publish, and a binding with no publish inside 48 hours is invalidated.

## The package clock and the impact clock.

The package clock is 2 October. Unvalidated trusted publishing configurations expire 48 hours after they are created. The same changelog says npm rejects trusted publishing tokens from GitHub Actions `issue_comment` events, next to the existing `pull_request_target` restriction. That is a separate control on which workflow events may publish. It is not the slug check. Bloom dates the fix "2.10" and links this changelog. Read 2.10 as 2 October, the page they linked, not as an npm release number.

The disclosure clock is the post's own timeline. Myers reported it through GitHub's HackerOne program on 6 June 2026. It was triaged and validated on 9 June. The last reply he cites is 8 July: the team was working on a fix and determining risk and severity. Six later enquiries, including a mediation request, went unanswered. Before publishing, Bloom sent the draft and ran the test again, which is how they found the fix. The team then apologized for the silence and said the fix was in place. The public post is 13 October. From 6 June to 2 October is 118 days. The numbers box in the same post says over 120 days. Count the dates.

The impact clock is Bloom's scan, and the post does not date it against the changelog. More than 1,500 packages, more than 10 million downloads in total, more than a million a month. Myers says that figure is among packages with trusted publishing configured, not the whole registry. He estimates adoption at under 20 percent of active packages. The scan section says they are not releasing names because the issue is still prevalent. The fix section says the 48-hour rule is already in place. Hold both sentences. A list withheld on 13 October is not a list you can diff, and it is not evidence those 1,500 bindings are still unpublished.

Myers told [Help Net Security](https://www.helpnetsecurity.com/2026/10/13/slugfest-npm-trusted-publishing-flaw/) it is unknown whether anyone exploited the flaw in the wild. The post's customer sentence is different: a few weeks ago Bloom blocked an agent that was trying to install one of the packages on the vulnerable list. An install of a package they had classed as exposed is not a malicious tarball.

The agent rates are Bloom's telemetry across dozens of organizations, not an npm census. Each agent brought in 0.3 to 1.2 new packages a day, and developer machines 2 to 3. An average endpoint ran about three agents, a developer machine five or more. That is about 70 direct agent-installed packages a month on a typical endpoint, and close to 350 on a developer machine, not counting dependencies. The title's "millions of AI agents" is the population Bloom says it studied. It is not a count of compromised installs.

GitHub's [transfer docs](https://docs.github.com/en/repositories/creating-and-managing-repositories/transferring-a-repository) retire an owner/name pair when the repository had more than 100 clones or more than 100 Actions uses in the week before the transfer, or when it contains a Marketplace action. Myers told Help Net that Bloom's set was the slugs that stayed claimable. Retirement on transfer is not the registry's check, and this brief did not re-read the rename and delete rules.

Bloom says the same slug gap was in npm's GitLab trusted publishing. The changelog's expiry sentence is about unvalidated configurations, not only GitHub. It does not say existing GitLab bindings were rewritten. The `issue_comment` rejection is a GitHub Actions event.

> The numbers box says the gap was exploitable for four months. The dates support a long window between report and changelog. They do not support a malicious version on a named package. Bloom did not publish one.

## A valid attestation can name the other repository.

Myers says a publish through the reclaimed slug still produces a Sigstore provenance attestation, and the attestation names the repository that ran the workflow. Downstream tooling that only checks whether a package is signed will call that attestation valid. The check that would have mattered is whether the repository in the attestation is the repository the maintainer still intends.

That is a different failure from a stolen publish token that runs inside the real repository. Here, if Bloom's account holds, the attestation is honest about a repository the package did not mean to trust. Reading "provenance: present" does not answer which repository.

## What to do with the binding.

Maintainers should run `npm trust list` for each package. The CLI page says that command needs npm 11.15.0 or newer. The [website docs](https://docs.npmjs.com/trusted-publishers/) say a package can hold up to 10 trusted publishers, and that an existing connection cannot be edited in place: delete it and create a new one. The CLI page says the registry currently supports one configuration per package. Those two official pages disagree. Read the list you actually have. If a GitHub or GitLab slug does not resolve to a repository you control, remove that binding and create one that does. Do not assume the 48-hour expiry rewrote a binding that had already published.

If you create a binding, publish from it inside two days. The docs say an unvalidated configuration expires and can no longer be used or edited. Recreate it. The same page says npm does not verify the configuration when you save it. A wrong filename or a wrong repository shows up when a publish is attempted, not when the form is saved.

For a package you install, there is no Bloom list to diff. Pin the version you reviewed. If you do check provenance, read the repository on the attestation. A new version that arrived through trusted publishing is still a new version. Bloom's blocked agent install is their telemetry, not a hash you can match.

Review GitLab project paths the same way. The changelog does not give them a separate sentence, and the CLI still asks for the path.

## Sources

- [SlugFest: Millions of AI Agents Install npm Packages Exposed to Malicious Takeover through GitHub](https://bloom.security/blog/slugfest-millions-of-ai-agents-download-exposed-npm-packages) (2026-10-13)
- [Unvalidated npm trusted publishing configurations now expire](https://github.blog/changelog/2026-10-02-unvalidated-npm-trusted-publishing-configurations-now-expire/) (2026-10-02)
- [Trusted publishing for npm packages](https://docs.npmjs.com/trusted-publishers/)
- [npm-trust](https://docs.npmjs.com/cli/v12/commands/npm-trust/)
- [npm Trusted Publishing flaw exposed 1,500+ npm packages to takeover](https://www.helpnetsecurity.com/2026/10/13/slugfest-npm-trusted-publishing-flaw/) (2026-10-13)
- [Trusted Publishers internals](https://github.com/pypi/warehouse/blob/main/docs/user/trusted-publishers/internals.md)
- [Transferring a repository](https://docs.github.com/en/repositories/creating-and-managing-repositories/transferring-a-repository)
