npm Trusted Publishing: OIDC Instead of Tokens

npm trusted publishing lets a CI job publish a package using a short-lived OpenID Connect token issued by GitHub Actions, GitLab CI or CircleCI, so no npm credential is ever stored in a secret. You register the workflow against the package on npmjs.com, give the job id-token: write, and run npm publish from npm CLI 11.5.1 or newer.

That is the whole idea, and it is now the only comfortable way to publish from automation. npm revoked every classic token on 9 December 2025. npm login hands you a two-hour session token that enforces 2FA on publish, and granular tokens with write scope are capped at a 90-day lifetime, so the old pattern of pasting an NPM_TOKEN into repository secrets and forgetting about it for three years is gone.

How do you set up npm trusted publishing on GitHub Actions?

Two halves, and both have to match exactly or the publish fails.

The registry half: open the package on npmjs.com, go to Settings, find the Trusted Publisher section, pick GitHub Actions, then fill in the organisation or user, the repository, and the workflow filename. That last field is the one people get wrong. It wants the filename only, with its .yml or .yaml extension, not a path, and it is case sensitive. You also choose which actions the publisher is allowed to perform: npm publish, npm stage publish, or both.

You can do the same thing from the terminal, which is worth knowing if you maintain more than one package:

npm trust github my-package \
  --repo whoooop/my-package \
  --file release.yml \
  --allow-publish

The CI half:

name: Release
on:
  push:
    tags: ['v*']

permissions:
  id-token: write
  contents: read

jobs:
  publish:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v7
      - uses: actions/setup-node@v6
        with:
          node-version: '24'
          registry-url: 'https://registry.npmjs.org'
          package-manager-cache: false
      - run: npm ci
      - run: npm publish

id-token: write is what allows the runner to mint the OIDC token. Note that permissions blocks are exhaustive: the moment you declare one, every permission you did not list drops to none, which is why contents: read is spelled out.

GitLab needs the token requested explicitly rather than granted by a permission:

publish:
  stage: publish
  image: node:24
  id_tokens:
    NPM_ID_TOKEN:
      aud: "npm:registry.npmjs.org"
    SIGSTORE_ID_TOKEN:
      aud: sigstore
  script:
    - npm ci
    - npm publish
  only:
    - tags

Why does it fail the first time?

A short list covers almost every failure.

The repository field in package.json has to point at the same GitHub repository you registered. A package whose repository.url still names an old org will be rejected even though the workflow and the runner are correct.

Only GitHub-hosted, GitLab.com shared and CircleCI cloud runners are supported. Self-hosted runners do not work yet. npm’s documentation lists that as planned, so if your release job runs on your own hardware, this is a blocker rather than a configuration problem.

A package can have exactly one trusted publisher. If a library is released from two workflows, one of them has to keep using a token or move into the other.

Private dependencies still need credentials. OIDC authenticates the publish, not the install, so a private npm ci in the same job wants a read-only granular token alongside it.

And npm whoami does not report OIDC status, so do not use it as a check that the setup worked. The publish either succeeds or it does not.

Once the workflow publishes cleanly, close the old door: in the package’s Settings under publishing access, choose the option requiring two-factor authentication and disallowing tokens, then delete the automation tokens you were using. Leaving them alive means you have added OIDC without removing anything.

Do you still need the provenance flag?

No. Publishing over OIDC from GitHub Actions or GitLab CI generates a provenance attestation automatically, with no --provenance flag, provided both the repository and the package are public. The attestation is signed through Sigstore and recorded in a public transparency log, which is what lets anyone verify the commit and workflow a tarball was built from.

Two exceptions worth knowing. CircleCI does not currently produce provenance, so trusted publishing there buys you the credential improvement and nothing else. And a private repository cannot generate provenance even when the publish itself is OIDC, because there is nothing public to attest to.

If you need to turn it off, NPM_CONFIG_PROVENANCE=false, a provenance=false line in .npmrc, or publishConfig.provenance in package.json all work.

On the consuming side, npm audit signatures validates registry signatures and provenance attestations across an installed tree. It is a reasonable CI step for a project with a large dependency surface.

Should CI publish, or only stage?

Staged publishing went generally available in May 2026 and pairs neatly with OIDC. Instead of npm publish, CI runs npm stage publish, which uploads the tarball to a staging area where it is not installable. A maintainer then runs npm stage list, npm stage view <stage-id> or npm stage download <stage-id> to inspect the exact artefact, and npm stage approve <stage-id> to release it. Approve and reject prompt for 2FA; publish, list, view and download do not.

It needs npm CLI 11.15.0 or later and Node 22.14.0 or higher.

The combination worth setting up deliberately: register the trusted publisher with npm stage publish permission only, and leave npm publish unchecked. CI can then build and upload a release candidate, but nothing reaches the registry until a human clears a 2FA challenge. A compromised workflow gets you a staged tarball nobody approved rather than a live malicious version.

What does trusted publishing not protect you against?

Your own publishes. That is the entire scope. Every package you install is somebody else’s publishing story, and most of the recent damage has come from the install side.

npm 12 changed the defaults there. allow-git and allow-remote now default to none, so git dependencies and remote tarball URLs are not resolved unless you set them back to all or root. That closes a path where a git dependency’s own .npmrc could override the git executable and run code even under --ignore-scripts. allow-file and allow-directory still default to all. Dependency lifecycle scripts are blocked unless the package is named in an allowScripts policy in your package.json, which you build up with npm install-scripts approve (still aliased as npm approve-scripts) after reviewing what --allow-scripts-pending lists. npm 12 also drops npm shrinkwrap and npm adduser, turns unknown .npmrc keys and unknown CLI flags into errors rather than warnings, and requires Node ^22.22.2 || ^24.15.0 || >=26.0.0.

pnpm attacks the same problem from a different angle with minimumReleaseAge, set in pnpm-workspace.yaml, which refuses to resolve a version until it has been on the registry for a given number of minutes. pnpm 11 defaults it to 1440, one day. Most compromised releases are pulled within hours, so a day of latency filters the majority of them without any judgement call on your part. Set it to 10080 for a week if your release cadence tolerates it, and remember to configure the matching cooldown in Renovate or Dependabot, otherwise they will keep opening pull requests for versions your install step will not fetch.

What I would actually do: move every published package to trusted publishing now, register it with staging permission rather than direct publish, and switch publishing access to disallow tokens once a release has gone through cleanly. Do the install-side work as a separate piece, because the npm 12 script policy will generate a round of failures in projects with native dependencies and you do not want that tangled up with a release pipeline change. The one case for staying on granular tokens is a self-hosted runner, and there the honest answer is a 90-day rotation reminder until npm ships support. We do this kind of pipeline and dependency hardening as part of our website security work, usually alongside locking down what a Node process can touch at runtime and tightening the CSP.

Need this built properly?

Whoooop Ltd has spent 15+ years building and maintaining web applications in TypeScript, React, Node.js and serverless — the same ground this post covers.

Get in touch