Adding --provenance to an existing npm publish command is not enough on its own. Many teams copy the flag into a workflow that has published successfully for years and hit:

npm error code EUSAGE
npm error Unable to generate provenance for @acme/[email protected].
npm error Provenance generation in GitHub Actions requires "write" access to the "id-token" permission

The flag depends on a permission most existing workflows never needed. This guide covers the exact permissions block, registry configuration, and job structure required to make Sigstore provenance generation succeed on the first try.


Root Cause

GitHub Actions workflows do not request an OIDC identity token unless a job explicitly declares id-token: write in its permissions block. npm’s CLI needs that token to exchange with Sigstore’s Fulcio certificate authority; without it, there is nothing to sign the provenance statement with, and the CLI aborts the publish rather than uploading an unsigned or falsely-attested tarball.

This is a deliberate fail-closed design: npm will not silently downgrade to a plain, unsigned publish when --provenance is requested but the identity token is unavailable.


Minimal Reproduction

This workflow reproduces the failure — it publishes correctly without --provenance, but fails the moment the flag is added, because the job’s permissions block never grants id-token:

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

jobs:
  publish:
    runs-on: ubuntu-latest
    permissions:
      contents: read   # id-token is missing here
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: 20
          registry-url: 'https://registry.npmjs.org'
      - run: npm ci && npm run build
      - run: npm publish --provenance --access public
        env:
          NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}

How the OIDC Exchange Fails Without the Permission

Missing id-token Permission Blocks Provenance A flow showing a workflow job with only the contents read permission attempting to request an OIDC token, being denied by GitHub's identity provider, and npm publish aborting before it can reach Sigstore's Fulcio authority. Job permissions contents: read only npm publish --provenance requests OIDC token Token request denied id-token permission absent Publish aborts EUSAGE — no attestation created

Step-by-Step Fix

Step 1 — Add the id-token: write permission to the job

 jobs:
   publish:
     runs-on: ubuntu-latest
-    permissions:
-      contents: read
+    permissions:
+      contents: read
+      id-token: write

Expected result: the job can now request a short-lived OIDC token scoped to this specific workflow run, repository, and ref.

Step 2 — Confirm the registry is configured before the publish step

   steps:
     - uses: actions/checkout@v4
     - uses: actions/setup-node@v4
       with:
         node-version: 20
+        registry-url: 'https://registry.npmjs.org'
     - run: npm ci && npm run build

Expected result: ~/.npmrc on the runner is populated with a registry entry; without it, npm cannot determine which registry to negotiate the provenance upload with, even if the OIDC exchange itself succeeds.

Step 3 — Re-run the publish step

npm publish --provenance --access public

Expected output:

npm notice Publishing to https://registry.npmjs.org/ with tag latest and default access
npm notice === Provenance Statement ===
npm notice sigstore verification successful
npm notice + @acme/[email protected]

HAZARD PREVENTION

Symptom: The fix above still fails with the same EUSAGE error.

Root cause: A workflow-level permissions block set at the top of the YAML file can be overridden by a more restrictive job-level block further down — GitHub Actions does not merge them additively.

Fix: Verify id-token: write is present on the job block itself (jobs.<job_id>.permissions), not only at the workflow root, and check that no matrix or reusable-workflow call downgrades it.


Verification

# Confirm the published version carries an attestation
npm view @acme/[email protected] dist.attestations.url

# Confirm registry + provenance signatures for a fresh install
npm ci
npm audit signatures

Expected output once the fix is applied:

audited 1 package in 0.4s
1 package has verified registry signatures

1 package has a verified provenance attestation

Edge Cases / Gotchas

  • Reusable workflows (workflow_call) need their own permissions block; the calling workflow’s id-token: write does not automatically propagate unless the reusable workflow also declares it.
  • Composite actions cannot declare permissions themselves — the permission must live on the job that invokes the composite action.
  • Pull requests from forks never receive a write-scoped OIDC token, by GitHub design, so provenance publishing must be restricted to trusted trigger events like tag pushes on the base repository, not pull_request.
  • Self-hosted runners work identically as long as the runner can reach https://token.actions.githubusercontent.com and Sigstore’s public infrastructure — outbound network policies in locked-down environments sometimes block this silently.
  • Monorepos with multiple publishable packages need id-token: write on every job that runs npm publish --provenance, not just the first one in a matrix.

Frequently Asked Questions

Do I need a paid GitHub plan to use id-token: write?

No. OIDC token issuance and the id-token: write permission are available on GitHub’s free tier for both public and private repositories. Provenance itself, however, is only accepted by the npm registry for packages published with public access.

Can I add --provenance to an existing workflow without touching permissions?

No. Adding the flag alone does nothing if id-token: write is absent — npm publish fails during the OIDC exchange. The permission must be declared explicitly; GitHub Actions does not grant it by default even to workflows that already have contents: write.

Does provenance work with reusable workflows or composite actions?

Yes, but the calling workflow must pass permissions through explicitly. A reusable workflow invoked with workflow_call needs its own permissions block, and the caller must not restrict id-token below write, or the OIDC token request will be denied.

Why does provenance fail specifically for a fork’s pull request build?

GitHub does not grant write-scoped OIDC tokens to workflows triggered by pull_request events from forks, as a security boundary. This prevents an external contributor’s PR from publishing a package under your identity. Provenance publishing must run from a trusted context such as a tag push on the base repository.

Is there a way to test provenance generation without actually publishing?

Not directly against Sigstore’s production Fulcio instance, since certificate issuance is tied to a real publish attempt. You can validate the surrounding configuration — permissions, registry-url, and package.json fields — with npm publish --dry-run --provenance, which reports configuration errors without contacting the registry.



↑ Back to npm Provenance & Sigstore Attestation