Mohit's blog

Why I Stopped Using npm Tokens in GitHub Actions

Hero.jpg
Published on
//
7 mins read
/
––– views

I recently found out about npm's Trusted Publishers, and my immediate reaction was:

wait, this is so much cooler than the token method.

If you have ever published an npm package from GitHub Actions, you already know the old ritual.

  • create an npm token
  • copy it into GitHub secrets
  • wire it into CI
  • pray it never leaks
  • completely forget about rotating it

It works. But it has always felt slightly cursed.

A long-lived publish token sitting in CI is one of those things we all accepted because it was normal, not because it was actually a good idea.

I recently switched @kubeorch/cli to trusted publishing, and after implementing it, I genuinely don't want to go back.

The old npm token flow always felt wrong

The classic GitHub Actions setup usually looks something like this:

- name: Publish to npm
  run: npm publish --access public
  env:
    NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}

Simple? Yes.

Great security model? Not really.

The problems are kind of obvious once you say them out loud:

  • the token is long-lived
  • it has to be manually created
  • it lives in CI secrets
  • it can be leaked by bad logging, bad workflow design, or just plain human error
  • if it leaks, someone else may be able to publish your package

And for npm, that is not a small problem.

If someone gets publishing access to your package, that is not just "oops, secret got exposed." That can turn into a full supply-chain mess very quickly.

Malicious package version. Compromised users. Broken trust. Incident cleanup. Apology tour.

That is way too much risk to attach to one static secret you probably set up months ago and forgot existed.

Trusted publishing changes the model completely

This is the part I like.

With trusted publishing, npm no longer relies on a long-lived token stored in GitHub secrets.

Instead, npm trusts a specific GitHub repository and workflow identity.

So when your workflow runs, GitHub Actions uses OIDC to prove:

  • which repository the workflow came from
  • which workflow is running
  • that this is a real CI execution and not some random person holding a leaked token

That means the mental model changes from:

whoever has the token can publish

to:

only this trusted workflow from this trusted repo can publish

That is just a way better default.

What I changed in kubeorch/cli

I set this up in the KubeOrch CLI release workflow.

The important part is that the publish job now allows GitHub Actions to request an identity token:

permissions:
  contents: read
  id-token: write

And the actual publish step looks like this:

- name: Publish to npm
  working-directory: ./npm-package
  run: npm publish --provenance --access public

That id-token: write permission is the key.

That is what enables the GitHub Actions workflow to mint the short-lived OIDC token npm uses for trusted publishing.

And --provenance is a really nice bonus here too, because now the publish includes provenance metadata instead of just "trust me bro, CI did it."

The nice part is what's missing

Here is the kind of release workflow that feels much better to me now:

name: Release and Publish
 
on:
  push:
    tags:
      - 'v*'
 
permissions: {}
 
jobs:
  release:
    runs-on: ubuntu-latest
    permissions:
      contents: read
      id-token: write
 
    steps:
      - uses: actions/checkout@v4
 
      - uses: actions/setup-node@v4
        with:
          node-version: 20
          registry-url: 'https://registry.npmjs.org'
 
      - name: Publish to npm
        working-directory: ./npm-package
        run: npm publish --provenance --access public

What I like most is what you don't see anymore.

  • no NPM_TOKEN
  • no NODE_AUTH_TOKEN
  • no secret copy-pasting
  • no "did we remember to rotate this thing?"

The workflow identity itself becomes the credential.

That feels much more correct.

Why this is actually more secure

People say "more secure" all the time, so let me make it less vague.

1. There is no long-lived npm publish secret sitting in CI

This is the biggest win.

If your GitHub secrets get dumped, or a workflow accidentally exposes environment variables, there is no permanent npm publish token just sitting there waiting to be abused.

That alone makes this better.

2. Publishing is tied to your repository identity

npm is not just accepting some random string token.

It can verify that the request came from the exact GitHub Actions workflow you configured.

That is a much stronger model than "someone knew the secret."

3. It removes a lot of human error

Token-based publishing depends on developers doing secret management well.

Let's be honest. That's not where most teams are strongest.

People create a token once, put it in secrets, and never think about it again until something breaks.

Trusted publishing removes that whole maintenance burden.

4. Provenance makes the release story better too

This is another underrated part.

Using:

npm publish --provenance --access public

means the published package includes build provenance.

That is a much healthier direction for package ecosystems in general, especially after all the supply-chain nonsense we've seen over the past few years.

Why I think this was introduced now

My guess is pretty simple: the ecosystem had to learn the hard way.

Too many secrets in CI. Too many leaks. Too many package incidents. Too many workflows built around static credentials that quietly became high-value targets.

Once you look at it from that angle, the old token-based model starts to look kind of outdated.

Why should publishing a package depend on a secret that was manually created months ago and stuffed into a CI settings page?

Why is that the thing protecting your package lineage?

Trusted publishing feels like the obvious evolution:

  • fewer secrets
  • shorter trust window
  • stronger identity guarantees
  • better auditability

And the best part is that it is not one of those security improvements that makes everything more annoying.

It actually feels cleaner.

Tokens vs trusted publishing

If I had to explain it to another maintainer in one minute, I'd say this.

Token-based npm publishing

  • easy to understand
  • easy to set up
  • easy to forget about
  • easy to leak
  • annoying to rotate
  • too much trust in one static credential

Trusted publishing

  • slightly newer mental model
  • much cleaner CI setup
  • much better security story
  • no long-lived npm token in GitHub secrets
  • feels much more aligned with modern infra practices

That last part matters.

A lot of modern infra has already moved away from static credentials and toward short-lived identity-based access.

npm trusted publishers feels like package publishing finally catching up.

If you maintain an npm package, I would seriously consider switching

Especially if:

  • your package is public
  • other people install it
  • you already release through GitHub Actions
  • you care about supply-chain security even a little bit

The setup is not complicated.

And once it is done, the release flow actually feels simpler than the token-based version.

That is the rare part here.

Usually better security means more pain.

This one actually reduces pain.

Final thought

After switching @kubeorch/cli to npm trusted publishing, my honest takeaway is this:

this feels like the version of npm publishing we should have had from the start.

Long-lived publish tokens in CI were always a little sketchy. We just got used to them.

Trusted publishing is cleaner, more secure, and a much better fit for how CI/CD should work in 2026.

If you maintain an npm package and you are still using an npm token in GitHub Actions, I think this is worth a look:

I found it recently, implemented it in KubeOrch CLI, and now the old token method just feels unnecessarily risky.

And honestly? Good riddance.