Key Takeaway
On 2 October 2026 npm began expiring trusted publishing configurations that have not completed a first successful publish within 48 hours, and started rejecting trusted publishing tokens from issue_comment events as well as pull_request_target. Release from push, release or workflow_dispatch, run npm trust list, and recreate any binding pointing at a repository you no longer control.
I went to set up a new trusted publisher for a small package this week, and the npm docs page told me something I had not read before: if the first publish does not happen within two days, the configuration expires and cannot even be edited. Delete it, create it again, and publish within two days. That sentence is the visible end of a much bigger story.
This post reads the GitHub changelog of 2 October 2026, the npm trusted publishers documentation, the npm trust command reference and the GitHub Actions security guide, then the Bloom Security research that explains why the change was needed. I quote a number only where I read it in one of those pages. Where I am reasoning rather than citing, I say so.
Two changes shipped on 2 October
The GitHub changelog entry makes two separate changes. The first is a lifetime for unproven trust: an unvalidated trusted publishing configuration now expires 48 hours after creation and can no longer authorise publishing. The second is a narrower list of workflow events: npm now rejects trusted publishing tokens from GitHub Actions issue_comment events, adding to the restriction that already covered pull_request_target.
Neither change touches a release that already works through a validated configuration triggered by a tag push. They bite in three places: a freshly created configuration that sits unused for two days, a configuration whose repository or project identity you change, and any workflow that publishes from a comment command. The table below is the whole rule set as the changelog states it.
| Surface | Rule from 2 October 2026 | What you do |
|---|---|---|
| Unvalidated configuration | Expires 48 hours after creation and can no longer authorise publishing | Run the first real publish inside the window |
| Validated configuration | Becomes exempt from expiry after its first successful publish | Nothing, but audit it anyway |
| Repository or project identity change | Needs a new trust relationship with a fresh 48-hour window; ordinary edits do not restart the deadline | Plan a rename or transfer around a new binding |
| issue_comment token | Rejected, in addition to the existing pull_request_target restriction | Move publishing to push, release or workflow_dispatch |
| Expired configuration | Stays visible in settings, does not count toward per-package limits, other configurations are unaffected | Recreate it to start a new 48-hour window |
The flaw behind the clock: a slug is not an identity
The 48-hour rule is a patch for a design weakness that Bloom Security named SlugFest. According to the CSO Online and Help Net Security write-ups, npm recorded the linked GitHub repository by its owner and name, not by the permanent numeric IDs that GitHub includes in the OIDC identity token, and npm did not check them. When a repository is renamed, transferred or deleted, the old name can be claimed by someone else, who then creates a repository at that address with a workflow of the expected filename.
That person never needs the maintainer's GitHub or npm credentials. Bloom demonstrated the attack on packages and repositories it controlled, and Help Net Security reports the malicious release carried a valid provenance attestation that named the attacker's repository. Bloom counted more than 1,500 potentially affected packages with over 10 million combined downloads, and said it had not observed exploitation in the wild.
Be careful about what the fix is. The changelog says GitHub limits the risk of trusting a repository or project name that changes ownership by expiring configurations that were never validated. It does not say existing validated bindings were retroactively tied to repository IDs, and Help Net Security lists exactly that as an open question. Bloom also says the GitLab integration has the same weakness. So the fix narrows the window; it does not remove your need to look.
How the 48-hour lifecycle behaves
The npm docs describe a new configuration as needing its first successful publish within 2 days; if it is not validated in that window it expires and can no longer be used or edited, and you delete it, create a new one and publish within 2 days. Two details from the docs matter for planning. npm does not verify a configuration when you save it, so a typo in the workflow filename only shows up at publish time, and existing connections cannot be changed at all.
| State | Can authorise a publish | How it leaves the state |
|---|---|---|
| Unvalidated, inside 48 hours | Yes, for the first publish | First successful publish validates it |
| Validated | Yes | Exempt from expiry; an identity change creates a new relationship instead |
| Unvalidated, past 48 hours | No | Recreate it; the expired entry stays visible but is not counted toward limits |
| Identity changed | Only through the new relationship | Fresh 48-hour window starts from creation of the new relationship |
The practical reading is that creating a trusted publisher and publishing are now one operation in your calendar. Do not set bindings up on a Friday for a release you plan to cut next week. Create it when the version is built, tagged and ready to go out.
Why issue_comment was a bad place to publish from
The changelog does not explain its reasoning for rejecting issue_comment, so what follows is my reading, anchored on the GitHub docs. An issue_comment workflow only starts if the workflow file exists on the default branch, and its GITHUB_REF is the default branch with GITHUB_SHA the last commit there. That means a comment event runs privileged, default-branch workflow code, yet the event is raised by whoever can comment, which is a far wider group than whoever can push a tag.
GitHub's own secure use guide already says to avoid pull_request_target where it is not necessary and warns that these privileged triggers must not explicitly check out untrusted code. The new npm rule extends the same instinct: publishing rights should hang off events your maintainers cause, not events a stranger can cause by typing.
| Event | npm trusted publishing token | Who raises it, in my reading |
|---|---|---|
| push (a tag), release | Permitted per the changelog | Someone with write access cut a tag or release |
| workflow_dispatch | Permitted per the changelog | A maintainer pressed a button or ran gh workflow run |
| issue_comment | Rejected since 2 October 2026 | Anyone able to comment, running default-branch workflow code |
| pull_request_target | Rejected before that date | Anyone who opens a pull request, with base-repository privileges |
If your release process is a bot reacting to a comment such as /release, it will fail at the npm publish step after 2 October. Do not look for a way to keep the comment trigger. Let the comment only open a draft or dispatch nothing, and publish from a tag or a manual dispatch that a maintainer controls.
Migrating the release workflow
The shape that survives the change is the one in the npm docs: a tag-triggered workflow with id-token write permission, a cloud-hosted runner, a recent Node, and no cache. Here is the wrong version I have seen in comment-driven release bots, followed by the right one built from the docs example.
# Wrong: a comment-driven release bot. After 2 October 2026 npm rejects the
# trusted publishing token minted from an issue_comment event, so the
# publish step fails even though the workflow file is on the default branch.
name: release-on-comment
on:
issue_comment:
types: [created]
permissions:
id-token: write
contents: read
jobs:
publish:
if: contains(github.event.comment.body, '/release')
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- run: npm ci && npm publish# Right: .github/workflows/release.yml
# The filename here is what the trusted publisher on npm must name, with the
# .yml extension. npm does not check it when you save the binding.
name: release
on:
push:
tags:
- 'v*' # a tag only someone with write access can push
workflow_dispatch: # manual re-run by a maintainer
permissions:
id-token: write # the critical requirement: lets the job mint the OIDC token
contents: read
jobs:
publish:
runs-on: ubuntu-latest # cloud-hosted only; self-hosted is not supported yet
environment: npm-publish # optional; must match the environment on npm if set
steps:
- uses: actions/checkout@v6
- uses: actions/setup-node@v6
with:
node-version: '24' # trusted publishing needs 22.14.0+
registry-url: 'https://registry.npmjs.org'
package-manager-cache: false # never restore a cache in a release
- run: npm ci
- run: npm test
- run: npm publish # provenance is automatic for a public repo and packageThree details in the right version are deliberate. The permissions block grants only id-token write and contents read, the docs example sets package-manager-cache to false because a release build should never restore a cache, and the optional environment lets you attach GitHub deployment protection, which you then also name in the trusted publisher. If you call the publish from a reusable workflow, the docs say validation checks the calling workflow's name and id-token write must be set in both parent and child.
Requirements from the docs: npm CLI 11.5.1 or later and Node.js 22.14.0 or higher for trusted publishing, and only cloud-hosted runners are supported, with self-hosted runners listed as planned. Provenance is generated automatically for a public repository and a public package, so you no longer pass the provenance flag.
Auditing the bindings you already have
Help Net Security's maintainer guidance is short: list your trust relationships, confirm each repository still resolves to one you control, delete and recreate any binding that points at a renamed or transferred repository, and remove the binding before deleting a repository. The npm trust command does this from a terminal. The docs say it needs npm 11.15.0 or above, write permission on the package and account-level 2FA, and that granular tokens with the bypass 2FA option are not supported.
# Needs npm 11.15.0+, write access to the package and account-level 2FA.
npm install -g npm@^11.15.0
for pkg in @acme/widget @acme/widget-cli; do
echo "== $pkg"
# Prints each trust relationship: repository, workflow file, environment, id.
npm trust list "$pkg"
done
# For every repository that list names, ask GitHub who it is NOW.
# Compare the full_name with what npm lists; a 404 or a different owner means
# the slug has moved or been freed, which is exactly the SlugFest precondition.
gh api repos/acme/widget --jq '{id, full_name}'
# Recreate a suspect binding: revoke the old id, then create the new one and
# publish within 48 hours so it validates.
npm trust revoke @acme/widget --id=<trust-id>
npm trust github @acme/widget --file release.yml --repo acme/widget \
--env npm-publish --allow-publish -yRun the loop below for each package you own. The GitHub API call returns the repository's numeric id and its current canonical name, which is exactly the identity the slug approach never pinned. If the name printed is not the one npm lists, or the call returns a 404, treat the binding as suspect and recreate it. If you consume packages, the same guidance says to check that a package's provenance attestation names the repository the project should come from.
Where this goes wrong, and what is still unknown
The named failure mode is the cold first publish. Because configurations expire in 48 hours, a binding created ahead of time for a quarterly release is dead on arrival, and npm will not tell you at save time because it does not verify a configuration until you publish. The second trap is the rename: a repository rename or transfer is now an identity change, so you create a new relationship and publish inside a fresh window, which means you cannot rename on the morning of a release.
The trade-off is real. You swap a standing, quiet binding for one that demands attention at creation, and a team with a slow release train will feel it. I take that trade, because an expiring binding that nobody finished validating is exactly the shape of the exposure Bloom described.
What I could not confirm from any source I read: whether existing validated configurations were retroactively bound to repository IDs, whether the flaw was ever exploited, and why issue_comment specifically was added. Treat those as open, and keep auditing rather than assuming the platform closed the hole for you.
What to do on Monday
Work through this in order. It is an hour of effort for a small account and a day for an organisation with many packages.
- Run npm trust list on every package you publish and write down each linked repository and workflow filename.
- Resolve each repository with the GitHub API; delete and recreate any binding whose repository was renamed, transferred or deleted.
- Search your workflows for issue_comment and pull_request_target near any npm publish, and move publishing to push, release or workflow_dispatch.
- Create new trusted publishers only when a release is ready, then publish inside 48 hours so the configuration validates.
- Verify the provenance attestation on your next release names your repository, and make that a release checklist line.
If you want the broader picture of how the keyless publishing path fits into pipeline authentication, my posts on GitHub Actions OIDC and on the Shai-Hulud style npm worms cover the neighbouring ground.
Trust that is never exercised should not live forever. npm now enforces that for trusted publishing by expiring unvalidated bindings after 48 hours and by refusing tokens from comment-triggered workflows, but the rule I carry away is simpler: bind to a repository you can prove you control, publish from an event only your maintainers can raise, and re-check the binding whenever a repository changes hands.
Sources
- GitHub Changelog: Unvalidated npm trusted publishing configurations now expire (2 October 2026)
- npm Docs: Trusted publishing for npm packages
- npm Docs: npm trust command reference
- GitHub Docs: Events that trigger workflows
- GitHub Docs: Secure use reference for GitHub Actions
- Help Net Security: npm Trusted Publishing flaw exposed 1,500+ npm packages to takeover
- CSO Online: Over 1,500 npm packages exposed by trusted publishing validation flaw

