Until recently, publishing a package from CI meant one thing: generate an npm automation token, paste it into your repository secrets, and hope.
That token is a credential with publish rights to your package, valid until someone remembers to rotate it. It sits in a settings page that every repository admin can read, gets copied into a second repository when you split the monorepo, and survives the departure of whoever created it. The supply-chain incidents of the last few years — event-stream, ua-parser-js, coa — all end at a stolen or misused publish credential.
Trusted publishing removes it.
How it works
The mechanism is OpenID Connect. GitHub Actions can mint a short-lived token describing the workflow that is running: this repository, this workflow file, this ref, this run. It is signed by GitHub and valid for minutes.
npm accepts that token in place of a password, after you tell it which workflow to trust. The exchange looks like this:
- Your workflow requests an OIDC token from GitHub.
- It presents that token to npm.
- npm verifies the signature, checks the claims against the trusted publisher you configured, and issues a publish grant for that run.
No secret is stored anywhere. There is nothing in your repository settings to steal, because the credential does not exist until the workflow runs and stops existing when it finishes.
Setting it up
On npmjs.com, open your package, go to Settings, and add a trusted publisher: the GitHub organisation, repository, and workflow filename. The workflow name is part of the trust decision — a different workflow in the same repository cannot publish.
Then the workflow:
permissions:
id-token: write # mint the OIDC token
contents: read
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 22
registry-url: https://registry.npmjs.org
- run: npm ci
- run: npm publish
Note what is absent: no NODE_AUTH_TOKEN, no secrets.NPM_TOKEN. id-token: write is the only new thing, and it is a permission rather than a secret.
The permission block is worth pausing on. GitHub Actions defaults to a broad permission set; declaring permissions explicitly narrows this run to exactly what it needs. If you take nothing else from this post, take that — it costs three lines and limits the damage from any compromised action in your dependency tree.
Provenance comes along
Publishing this way attaches a provenance attestation: a signed statement of which repository, commit, and workflow produced the tarball, recorded in a public transparency log.
On the package page it appears as a "Built and signed on GitHub Actions" badge linking to the exact run. Anyone can verify it:
npm audit signatures
What this proves is narrow and worth being precise about. It proves the tarball on the registry was produced by that workflow from that commit. It does not prove the code is good, that the commit was reviewed, or that the workflow was not compromised. What it removes is one specific attack: a tarball that was never built from the source it claims. Before provenance, a stolen token let someone publish anything under your name and the registry page looked identical. Now the mismatch is visible.
Things worth knowing first
The first publish still needs a human. Trusted publishing is configured on an existing package, so version 0.0.1 goes up from a laptop with 2FA. Only afterwards can you wire the workflow.
Scoped packages need --access public on that first publish, or npm assumes private and fails on an account without a paid plan. This has nothing to do with OIDC and catches everyone once.
Forks cannot publish, which is the entire point. A pull request from a fork gets no id-token and no publish grant. This is correct and occasionally surprising when a maintainer's own fork-based PR "mysteriously" skips the release step.
npm ci needs a lockfile the CI's npm can read. Unrelated to trusted publishing, but it is where our first attempt actually died: the lockfile was written by npm 12 and the runner had npm 10, which rejects it with Missing: <pkg> from lock file. Pin the npm version in the workflow. An hour of "the OIDC setup is broken" turned out to be a version mismatch three steps earlier.
Worth it?
It took about twenty minutes, and the release pipeline now holds no credential that can be stolen. For a package other people install and run on their machines, that is not a nice-to-have.
The broader shape is the point: the industry is moving from long-lived secrets to short-lived, workload-bound identity, and it is happening everywhere — cloud providers, container registries, package registries. Learning the pattern once on a small package is a cheap way to meet it.