---
title: "Trusted publishing, token-less npm releases via GitHub OIDC | Recipe — StudioMeyer Academy"
description: "Replace NPM_TOKEN secrets with OIDC. publish.yml that triggers on tag push. npm provenance badges. Cleaner trust signal for security-conscious users."
canonical: "https://studiomeyer.academy/en/recipes/9.4-trusted-publishing"
language: en
markdown_versions: ["https://studiomeyer.academy/en/recipes/9.4-trusted-publishing.md"]
publisher: "StudioMeyer Academy, https://studiomeyer.academy (llms.txt: https://studiomeyer.academy/llms.txt)"
---

[← Alle Recipes](https://studiomeyer.academy/recipes)

Phase 9 · Distribution·8 steps

# Trusted publishing, token-less npm releases via GitHub OIDC

Replace NPM\_TOKEN secrets with OIDC. publish.yml that triggers on tag push. npm provenance badges. Cleaner trust signal for security-conscious users.

8 steps0%

Du liest ohne Account. Mit [Login](https://studiomeyer.academy/login?next=/recipes/9.4-trusted-publishing) speichern wir Step-Fortschritt + Notes.

# Trusted publishing, token-less npm releases via GitHub OIDC

Storing `NPM_TOKEN` as a GitHub secret has worked forever, but it's a long-lived credential, leak it and an attacker can publish malicious versions of your package. **Trusted publishing** swaps the static token for OIDC: GitHub Actions exchanges a short-lived signed token at publish time, no static secret on either side. This recipe is the 5-minute setup plus the npm provenance badge that tells users "this came from a verified GitHub Actions build".

### Step 1: Why this matters

Three failure modes the static token pattern allows:

- **Repo compromise**, `NPM_TOKEN` env var leaks through a malicious dep, attacker publishes `your-package@1.0.99` with a backdoor.
- **Maintainer compromise**, npm account stolen, attacker publishes from elsewhere.
- **Misuse drift**, the token gets reused in random scripts over time, eventually exposed.

OIDC fixes #1 and partially #3. The token is created at publish time, valid for ~10 minutes, scoped to the exact repo + workflow + ref. There's no static secret to leak.

`npm provenance` (the bonus) adds a verifiable claim to the published version: "this package was built by GitHub Actions in `you/your-mcp-server` from commit SHA abc123 at timestamp Y." Users can verify the chain.

### Step 2: Configure trusted publisher on npm

1. Go to https://www.npmjs.com/package/your-mcp-server/access (you must be the maintainer).
2. **Trusted Publisher Management** → **Add trusted publisher**.
3. Fill:
    - **Repository owner**: your GitHub username/org
    - **Repository name**: `your-mcp-server`
    - **Workflow name**: `publish.yml` (must match the file in `.github/workflows/`)
    - **Environment** (optional): `production` (recommended, adds another layer)
4. Save.

After this, npm will accept publishes only from this exact workflow / repo / branch combo.

### Step 3: The publish workflow

```yaml
# .github/workflows/publish.yml
name: Publish to npm

on:
  push:
    tags: ['v*']  # triggers on v1.0.0, v1.0.1, etc.

jobs:
  publish:
    runs-on: ubuntu-latest
    environment: production  # if you set the optional environment in Step 2
    permissions:
      contents: read
      id-token: write  # ← required for OIDC token exchange
    steps:
      - uses: actions/checkout@v4

      - uses: actions/setup-node@v4
        with:
          node-version: '22'
          registry-url: 'https://registry.npmjs.org'
          cache: 'npm'

      - run: npm ci
      - run: npm run build
      - run: npm test

      - run: npm publish --provenance --access public
        # No NODE_AUTH_TOKEN! OIDC handles auth via the id-token permission above.
```

Key bits:

- **`permissions.id-token: write`**, grants the workflow the right to request an OIDC token from GitHub. Without this, npm rejects.
- **No `NODE_AUTH_TOKEN`**, that's the whole point. Compare to the old pattern where this would be `${{ secrets.NPM_TOKEN }}`.
- **`--provenance`**, generates the signed provenance attestation. Requires `id-token: write`.
- **`--access public`**, required for scoped packages on first publish.
- **`npm test` before publish**, bad version doesn't ship.

### Step 4: Trigger a publish

```bash
npm version patch         # 1.0.0 → 1.0.1, creates git tag v1.0.1, commits
git push origin main --tags
```

The tag push triggers the workflow. Watch it under **Actions**:

```
Run npm publish --provenance --access public
npm notice Publishing to https://registry.npmjs.org/ with tag latest and public access
npm notice ✓ Provenance statement uploaded to https://search.sigstore.dev/
+ your-mcp-server@1.0.1
```

Sigstore link is the verifiable claim. Anyone can check it later.

### Step 5: Provenance badge in README

After your first provenance-enabled publish, add to README:

```markdown
[![npm provenance](https://img.shields.io/npm/v/your-mcp-server?label=npm%20provenance&color=brightgreen&logo=npm)](https://www.npmjs.com/package/your-mcp-server?activeTab=provenance)
```

The npm package page now shows a "Provenance" tab listing every release built via OIDC, with the source commit and workflow. Reviewers can verify the published bytes match the GitHub source.

### Step 6: Confirm setup

```bash
# 1. Check that OIDC publish worked (no token used)
gh run list --workflow=publish.yml --limit=1
# Look for "success" status

# 2. Verify provenance on npm
npm view your-mcp-server --json | jq '.dist'
# Should include `signatures` array
# Plus an `attestations` block referencing GitHub Actions

# 3. Public verification (anyone can run this)
npm audit signatures your-mcp-server
# verified registry signatures, audited Y packages in Xs
# Y packages have verified registry signatures
```

Client check · run on your machine

cat package.json 2>/dev/null | python3 -c "import json,sys; p=json.load(sys.stdin); print(\\"name:\\", p.get(\\"name\\")); print(\\"version:\\", p.get(\\"version\\")); print(\\"private:\\", p.get(\\"private\\"))" 2>/dev/null || echo "no package.json"

**Expect:** name + version are set, private is false (or omitted).

**If stuck:** Remove \`"private": true\`, set a unique name, then \`npm publish --access public\`.

### Step 7: Verify

Run `academy_validate_step`. The validator checks `npm view your-mcp-server version` returns a version.

For trusted-publishing specifically, the package page on npmjs.com should show:

- ✅ "Provenance" tab with attestations
- ✅ "Built and signed on GitHub Actions" badge
- ✅ Source commit visible per release

If you don't see those, the workflow either didn't request the OIDC token (`id-token: write` missing) or didn't pass `--provenance` to `npm publish`.

### Step 8: Migration from NPM\_TOKEN

If you currently publish with `NPM_TOKEN`:

1. Set up trusted publisher on npm (Step 2).
2. Update workflow to remove `NODE_AUTH_TOKEN` and add `id-token: write` + `--provenance` (Step 3).
3. Test with a `npm version prerelease` first to verify.
4. After successful provenance publish, **revoke the old `NPM_TOKEN`** in npm Settings → Access Tokens.
5. Remove the `NPM_TOKEN` GitHub secret in Settings → Secrets → Actions.

The old static token is now useless. If anyone leaked it, the leak is no longer dangerous, npm rejects it.

## Common traps

- **Forgetting `id-token: write`**, workflow runs but `npm publish` fails with auth error. Always include the permission.
- **Workflow filename mismatch**, npm trusted publisher config requires exact filename. If you renamed `publish.yml` → `release.yml`, update both sides.
- **Branch / tag mismatch**, if you set the trusted publisher to "main" branch but trigger on tag push, the OIDC claim ref doesn't match. Use `tags: ['v*']` trigger and configure the publisher accordingly.
- **`environment: production` set on workflow but not on npm**, npm rejects with "environment claim mismatch". Set both or neither.
- **`--provenance` without `id-token: write`**, error "OIDC unavailable". Fix permissions block.
- **Forgetting to revoke the old `NPM_TOKEN`**, security debt. After OIDC works, delete the static token.

## What good looks like

Tag push triggers the workflow, OIDC exchanges a short-lived token, `npm publish --provenance` succeeds, npm package page shows the Provenance tab with the signed attestation pointing at the exact commit. No long-lived secrets anywhere. Reviewers see the provenance badge and trust your supply chain a notch more.

For OSS MCP servers, trusted publishing is becoming the table-stakes signal. Set it up once and never think about NPM\_TOKEN rotation again.

[← MCP marketplaces, where to sub](https://studiomeyer.academy/recipes/9.3-mcp-marketplaces) [Writing a README that actually →](https://studiomeyer.academy/recipes/9.5-readme-that-converts)

Phase 9

npm publish, GitHub OSS, MCP marketplaces, trusted publishing

[Alle Phasen →](https://studiomeyer.academy/recipes)

Concepts

[MCPize](https://studiomeyer.academy/concepts/mcpize)

## Sitemap

Public pages of this site as Markdown: [sitemap](https://studiomeyer.academy/sitemap.md). Curated entry point: [llms.txt](https://studiomeyer.academy/llms.txt), full text: [llms-full.txt](https://studiomeyer.academy/llms-full.txt).
