Why Bother
npm shipped staged publishing on May 22, 2026, as a generally-available feature in npm CLI 11.15.0. The mechanic is simple: npm publish puts a package live; npm stage publish puts the package in a staging queue, where a human maintainer with a 2FA challenge must approve it before it goes live.
A stolen automation token can call npm stage publish. It cannot complete the approval. After the Shai-Hulud, TeamPCP, TanStack, and RubyGems supply chain attacks of the last six months, that single asymmetry is the cheapest meaningful supply chain defense available to any package maintainer.
The feature is opt-in. Until you change CI, your packages publish exactly as before. This article is the migration plan.
The Minimum Change For A Single-Package Repo
A typical GitHub Actions publish step:
- name: Publish to npm
run: npm publish --access public
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
Migrate by changing one word:
- name: Stage publish to npm
run: npm stage publish --access public
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
After the CI job succeeds, the package version sits in the staging queue. A maintainer opens npmjs.com (or runs npm stage list locally), reviews the staged version, and approves it:
npm stage approve my-pkg@1.4.2
# Interactive 2FA prompt
That is the full minimum migration. Two lines of CI changes, plus a person with a phone or hardware key to finalize the publish.
Add Trusted Publishing And Stage-Only Mode
The minimum migration leaves one gap: a direct npm publish call from anywhere (CI accidentally, a developer locally) still bypasses the staging queue. To close that gap, enable trusted publishing (OIDC-based authentication, no long-lived tokens) and configure the package as stage-only.
Set up trusted publishing through the npm web UI for your package:
- Configure a trusted publisher for your package on npmjs.com, pointing at your GitHub Actions workflow.
- Remove the long-lived NPM_TOKEN from your GitHub Actions secrets. The OIDC integration replaces it.
- Flip the package to stage-only in the npmjs.com publisher settings. Direct
npm publishis now rejected at the registry.
The updated workflow looks like:
- name: Authenticate with npm via OIDC
uses: actions/setup-node@v4
with:
registry-url: https://registry.npmjs.org/
- name: Stage publish to npm
run: npm stage publish --access public --provenance
No NPM_TOKEN anywhere in the workflow. No long-lived secret to steal. Every publish is OIDC-authenticated, attestation-signed, and gated by a human 2FA approval.
This is the configuration you want every maintainer of a high-impact package to reach by the end of the quarter.
Monorepo Adoption (Changesets, Lerna, Nx, Turborepo)
A monorepo that publishes 12 packages per release becomes 12 approvals when each one is staged. There is no built-in bulk approve UX yet. The practical options:
Option A: Approve each one in the npmjs.com web UI. Acceptable for small releases. Painful for large ones.
Option B: Stage from CI, approve from a local script that loops npm stage approve with the 2FA prompt firing once per call:
#!/bin/bash
for pkg in $(npm stage list --json | jq -r '.[].name'); do
echo "Approving $pkg..."
npm stage approve "$pkg"
done
This works but is interactive-only. Each approval call asks for a 2FA code. With hardware-key 2FA this is fast. With TOTP and 12 packages it is fifteen seconds of typing.
Option C: Limit staged publishing to your high-impact packages. Not every package in a monorepo is equally critical. A monorepo with two customer-facing packages and ten internal-shared packages can stage-publish the two and direct-publish the ten. Configure stage-only on the critical packages only.
For most teams shipping 1 to 4 customer-facing packages from a monorepo, Option C plus Option B for the staged subset is the right balance.
Release Tooling Adjustments
Automated release tools each have a small adjustment:
release-please. Out of the box release-please runs npm publish after merging the release PR. Override the publish command in your workflow:
- name: Stage publish via release-please
if: ${{ steps.release.outputs.release_created }}
run: npm stage publish --access public --provenance
semantic-release. Configure the @semantic-release/npm plugin with a pkgRoot and add a custom publish step in the GitHub Actions workflow. The plugin's default publish command can be replaced via a publishConfig step.
changesets. Add the stage step to the release script in package.json:
{
"scripts": {
"release": "changeset publish",
"release:staged": "changeset publish --no-git-tag && npm stage publish"
}
}
Or, more cleanly, use changesets to bump versions and write changelogs only, and add a separate workflow step that runs npm stage publish for each version-bumped package.
What Breaks And What Does Not
Adopting staged publishing breaks a few common assumptions.
Downstream consumers expecting the package live immediately after CI green. If your release notification (Slack, email, GitHub release) fires from the same workflow that calls npm stage publish, the notification arrives before the package is live. Move the notification to a post-approval workflow, or accept that "released to staging" and "released to npm" are two separate states.
npm view returning the new version. Until approval, npm view <pkg> versions does not show the staged version. Tools that poll for the new version need a slight delay or a manual trigger.
install --pre flag, pre-release tags, and dist-tags. Staging treats every dist-tag the same. A staged @beta publish still requires approval before the dist-tag updates.
What does not break:
- Existing installed versions. Anyone who already has the previous version installed sees no difference.
- Provenance. Staged publishes carry the provenance attestation, which transfers to the live version on approval.
- Download counters and stats. Staged versions are not counted until approved.
- Other registries. Staged publishing is npm-only. PyPI and Crates.io have their own evolving stories.
A Pre-Migration Local Test
Before flipping CI, do the migration locally on a throwaway test package:
# In a test project
npm version patch
npm stage publish --access public
# Verify the staged version
npm stage list
npm stage view test-pkg@1.0.1
# Approve and verify it goes live
npm stage approve test-pkg@1.0.1
npm view test-pkg version
Doing the dry run once on a real package answers a lot of "wait, how does this part work" questions before the CI migration locks in the new flow.
A Two-Week Adoption Plan
Realistic week-by-week:
Week 1
- Update CI runner images to npm 11.15.0 or later.
- Pick one high-impact package and migrate its workflow to
npm stage publish. - Document who on the team holds approval rights and what the approval response time is.
- Do one real release through the new flow.
Week 2
- Add trusted publishing OIDC to the migrated package.
- Flip stage-only mode on for that package.
- Remove the now-unused NPM_TOKEN secret.
- Add a second package to the migrated set.
- Write the approval runbook: who approves on weekdays, who approves during incidents, what the rollback procedure is.
Continue rolling out to additional packages on a one-per-week cadence. By the end of a quarter the package surface that justifies staged publishing is all migrated, and the long-lived token surface has shrunk meaningfully.
What Adoption Does Not Replace
Staged publishing closes one specific attack: stolen automation token publishes a poisoned version. It does not replace:
- Code review on the pull request that adds the publish.
- Dependency review and pinning for transitive supply chain risk.
- Secret scanning on the repository for credential leaks.
- 2FA on the npm account itself.
- Hardware-backed 2FA on critical maintainer accounts.
A team that adopts staged publishing without those is still exposed. Staged publishing is one layer of a defense-in-depth posture, not a replacement for the rest.
Bottom Line
Two lines of CI change, one OIDC configuration step, one stage-only flip, and one approval runbook. That is the full staged-publishing migration for a typical package. The asymmetry it produces (CI can stage, only a human can finalize) closes the cheapest mass-volume npm supply chain attack of the last year.
If your team is running a busy CI release pipeline and wants the migration done cleanly across all packages, with OIDC trusted publishing set up, stage-only enforcement configured, and the release-automation tooling adjusted in place, our CI/CD pipeline setup team handles exactly this kind of upgrade. The work is small. The defense it adds is meaningful.
Need help with this?
Our team handles this kind of work daily. Let us take care of your infrastructure.
Related Articles
The Ultimate Guide to Linux Server Management in 2025
A comprehensive guide to modern Linux server management covering automation, containerization, cloud integration, AI-driven operations, security best practices, and essential tooling for 2025.
Server & DevOpsFixing "421 Misdirected Request" for Plesk Sites on Ubuntu 22.04 After Apache Update
Resolve the 421 Misdirected Request error affecting all HTTPS sites on Plesk for Ubuntu 22.04 after an Apache update, caused by changed SNI requirements in the nginx-to-Apache proxy chain.
Server & DevOpsHow to Set Up GlusterFS on Ubuntu
A complete guide to setting up a distributed, replicated GlusterFS filesystem across multiple Ubuntu 22.04 nodes, including installation, volume creation, client mounting, maintenance, and troubleshooting.