If your GitHub Actions release publishes to npm with a NPM_TOKEN secret, it has an expiry date. From January 2027, a granular token that bypasses 2FA can no longer publish on its own (npm docs). Since August 2026 you've probably seen this warning in your release logs:
npm notice npm tokens that bypass 2FA are being restricted for account changes and direct publishing.
The replacement is trusted publishing. GitHub signs a short-lived OIDC token for your workflow run, npm exchanges it for a publish token, and nothing long-lived is stored anywhere. You also get a provenance badge on every release for free.
The idea is simple. The migration has a handful of sharp edges.
What actually has to change
For a typical release workflow:
-
Give the job an OIDC token. Add
permissions: id-token: writeto the publishing job. If the job had no permissions block before, keep whatever else it needs, such ascontents: writefor tagging. -
Use npm 11.5.1 or newer. This is the one that bites. Node 22 ships npm 10, so you need Node 24 or an
npm install -g npm@^12step before publishing. -
Remove the token. Delete
NODE_AUTH_TOKEN/NPM_TOKENfrom the publish step and the job. npm falls back to a configured token, so leaving it in keeps the old token in use. -
Set
registry-urlonactions/setup-node. -
Fix
repository.urlinpackage.json. npm checks it against the repository the workflow runs in. - Connect the package to the workflow on npm. Either open npmjs.com → package → Settings → Trusted publishing, or run:
npm trust github my-pkg --repo me/my-repo --file release.yml --allow-publish --yes
The workflow file name must match exactly. With reusable workflows, it's the calling workflow's file.
- Delete the secret after the first tokenless release.
The errors you'll see if one step is missing
These messages don't point at the cause. Here's what each one usually means.
npm error code ENEEDAUTH
No id-token: write, npm older than 11.5.1, or a workflow file name that doesn't match the trusted publisher exactly (it's case-sensitive and includes .yml).
npm error 404 Not Found - PUT https://registry.npmjs.org/...
Usually the same causes as above, or a GitHub environment that doesn't match the one on the trusted publisher. It's also what an expired token looks like, which is how most people find out.
npm error code E422 ... repository.url
package.json repository is missing, or doesn't match the GitHub repository.
Doing it in one command
To make this a one-liner, I built go-tokenless (MIT). From the root of the repo:
npx go-tokenless # preview: lists every change and shows a diff, writes nothing
npx go-tokenless apply # makes the changes
It edits only the lines that need changing, keeping your comments and layout, and then prints the exact npm trust github … command for each package. It handles npm, pnpm, Yarn Berry, changesets, semantic-release, release-please, Lerna/Nx and reusable workflows. It also follows publishes hidden inside ./scripts/*.sh, make targets and composite actions.
A few things it deliberately won't do. In these cases it stops and explains instead of guessing:
-
Fork-reachable publishing: it won't grant
id-token: writeto a publish job in a workflow triggered bypull_request_target,issue_commentorworkflow_run. That would let a fork publish your package. - Self-hosted runners: it won't migrate a job on a self-hosted runner, because npm's trusted publishing doesn't accept them.
- Ambiguous edits: it won't touch anything when an edit would change more than the migration. It compares the workflow before and after, by meaning, before writing.
If your installs need private packages from your npm org, --read-token NPM_READ_TOKEN gives the install steps a read-only token, and publishing stays tokenless.
For AI coding agents
It ships as an MCP server and an Agent Skill too, so you can just ask your agent to "move our npm publishing to trusted publishing":
claude mcp add go-tokenless -- npx -y go-tokenless mcp
Before January
Run npx go-tokenless in your repos. It's read-only, takes seconds, and tells you whether each one is already tokenless. If it isn't, the diff is usually five lines.
Disclosure: I maintain go-tokenless. Issues and PRs welcome: https://github.com/Continuous-Actions/go-tokenless
Top comments (0)