After the recent npm supply chain attacks, long-lived tokens are out. Trusted publishing via OIDC is the way forward. But if you maintain scoped packages (@org/package), you're going to hit some walls the docs don't warn you about.
I spent a full day getting @edictum/openclaw to publish via trusted publishing from GitHub Actions. Here's everything I learned so you don't have to.
The Setup
I maintain Edictum, a runtime contract enforcement library for AI agents. We just shipped a native OpenClaw plugin and needed to publish @edictum/openclaw to npm from GitHub Actions using trusted publishing — no tokens, no secrets.
What Worked
Here's the final working workflow:
name: Publish to npm
on:
release:
types: [published]
permissions:
id-token: write
contents: read
jobs:
publish:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v5
- uses: pnpm/action-setup@v4
- uses: actions/setup-node@v5
with:
node-version: 22
registry-url: https://registry.npmjs.org
cache: pnpm
- run: pnpm install --frozen-lockfile
# THIS IS CRITICAL — bundled npm doesn't support OIDC for scoped packages
- run: npm install -g npm@latest
- run: pnpm build
- run: pnpm test
- run: npm publish --provenance --access public
And in package.json:
{
"publishConfig": {
"access": "public",
"provenance": true,
"registry": "https://registry.npmjs.org/"
},
"repository": {
"type": "git",
"url": "git+https://github.com/edictum-ai/edictum-openclaw.git"
}
}
The Problems (and Fixes)
1. Bundled npm doesn't support OIDC for scoped packages
This is a known issue. The npm version bundled with Node.js 22 on GitHub-hosted runners is too old. You must upgrade:
- run: npm install -g npm@latest
The npm docs say you need npm 11.5.1+. Without this, you get:
npm error code E404
npm error 404 Not Found - PUT https://registry.npmjs.org/@scope%2fpackage - Not found
This E404 is misleading — the package exists, but the old npm can't do the OIDC token exchange for scoped packages.
2. actions/setup-node injects a default token that breaks OIDC
When you use registry-url with actions/setup-node, it automatically sets NODE_AUTH_TOKEN to ${{ github.token }}. This GitHub token overrides the OIDC flow, and npm tries to authenticate with it instead of doing the OIDC exchange.
Some people clear it with NODE_AUTH_TOKEN: "", but that breaks authentication entirely (ENEEDAUTH). Others remove registry-url, but then npm doesn't know where to publish.
The fix that actually works: upgrade npm (step 1 above). The upgraded npm correctly handles the OIDC flow even with the injected token present. The --provenance flag triggers the OIDC path explicitly.
3. The environment field on npmjs.com must match (or be empty)
When configuring the trusted publisher on npmjs.com, there's an "Environment name" field. If you set this to npm or release, the GitHub Actions job must run in a matching GitHub environment.
What tripped me up: I set the environment name on npmjs.com to npm and added environment: npm to my workflow job. It still failed with E404. Removing the environment name from npmjs.com (leaving it blank) fixed it immediately.
If you need environment-based protection (approval gates, etc.), make sure the names match exactly — case-sensitive, no trailing spaces. But if you don't need it, leave it blank.
4. --provenance is NOT automatic (despite what docs say)
The npm docs state:
When you publish using trusted publishing, npm automatically generates and publishes provenance attestations. You don't need to add the
--provenanceflag.
This was not my experience. Publishing without --provenance resulted in ENEEDAUTH. Adding it fixed the issue. You can also set it in package.json:
{
"publishConfig": {
"provenance": true
}
}
5. repository.url must match your GitHub repo exactly
The trusted publisher config on npmjs.com requires your org/repo. Your package.json must agree:
{
"repository": {
"type": "git",
"url": "git+https://github.com/your-org/your-repo.git"
}
}
If these don't match, npm silently rejects the publish with a 404.
How Big Projects Handle This
Curious how others do it, I checked several major open-source projects:
| Project | Method |
|---|---|
| Vercel AI SDK |
NPM_TOKEN secret (token-based) |
| LangChain.js |
NPM_TOKEN secret + manual .npmrc
|
| OpenClaw | OIDC trusted publishing (non-scoped package) |
| shadcn/ui |
NPM_TOKEN secret |
Most major projects still use token-based publishing. The ones using OIDC successfully tend to be non-scoped packages. Scoped packages with OIDC are still rough — npm/cli#8976 is open as of today.
The Checklist
If you're setting up trusted publishing for a scoped npm package from GitHub Actions:
- [ ] npm 11.5.1+ (
npm install -g npm@latestin your workflow) - [ ]
permissions: id-token: writein your workflow - [ ]
registry-url: https://registry.npmjs.orginactions/setup-node - [ ]
--provenanceflag onnpm publish - [ ]
repository.urlinpackage.jsonmatches your GitHub repo - [ ]
publishConfig.access: "public"inpackage.json(for scoped packages) - [ ] Trusted publisher configured on npmjs.com → package → Settings
- [ ] Environment name on npmjs.com: leave blank unless you specifically need it
- [ ] No
NODE_AUTH_TOKENsecret set in your repo (it would override OIDC)
The Result
@edictum/openclaw is now published with OIDC trusted publishing and provenance attestations, no long-lived secrets anywhere:
npm i @edictum/openclaw
Or if you're an OpenClaw user:
openclaw plugins install @edictum/openclaw
One command, zero code changes, 25 security contracts active. Check it out: github.com/edictum-ai/edictum-openclaw
Edictum is runtime contract enforcement for AI agent tool calls. Deterministic YAML contracts that execute outside the model — the LLM can't talk its way past them. Available in Python, TypeScript, and Go.

Top comments (0)