Hook
npm just tightened two painful parts of the JavaScript supply chain at the same time: the final package publish step and the sources from which a project is allowed to install dependencies.
Staged publishing is now generally available, and npm CLI 11.15.0+ added new allow-* controls for file, remote, and directory sources. Together with allow-git, this is not just a feature announcement. It is a reason to review the release pipeline.
Problem / Context
Many teams have used the same flow for years: CI builds the package, runs tests, and executes npm publish. That is convenient until something goes wrong. If the CI token is too broad, the workflow is compromised, or the release job runs from the wrong context, the package immediately reaches the registry.
Staged publishing adds a pause between automated package preparation and final publication. CI can run npm stage publish, but the last step is approved by a human through the CLI or npmjs.com. The strongest setup combines this with trusted publishing, restricted token access, and stage-only permissions.
The second change is about install. Dependencies do not always come from the registry. A package.json can reference git URLs, local files, directories, or remote tarballs. Sometimes that is intentional. Sometimes it is a quiet bypass around normal dependency review.
The new allow-file, allow-remote, and allow-directory options complement allow-git. Each control can be all, none, or root, so policy does not have to mean “break everything” or “allow everything”.
Why It Matters
Publish and install are two sides of the same risk. Publish decides what the team sends out. Install decides what the team brings in.
If CI can publish directly, one broken workflow can become a production incident for every user of the package. If install freely allows non-registry sources, a project may pull in a dependency that is harder to review, reproduce, or update.
For libraries, this is a reputation risk. For application repositories, it is a risk of hidden dependencies bypassing normal registry review. For platform teams, it is also a repeatability issue: a build should be understandable a month later, not depend on a surprising URL or local directory.
How To Do It
1. Find direct publish
Start by searching release jobs:
rg "npm publish|npm stage publish|NPM_TOKEN|NODE_AUTH_TOKEN" .github .gitlab-ci.yml package.json .npmrc
If you see npm publish in CI, ask one question: can this job change the public registry directly, or can it only prepare a release?
2. Upgrade the runtime for stage flow
Staged publishing requires npm CLI 11.15.0+ and Node.js 22.14.0+. Pin that in CI instead of relying on a floating image:
- uses: actions/setup-node@v4
with:
node-version: 22.14.0
- run: npm -v
Then replace final publish in CI with:
npm stage publish --provenance
Keep final approval with a human using 2FA. The goal is not to make every release slow. The goal is to stop CI from being the last authority before the registry.
3. Narrow CI permissions
The cleanest model is trusted publishing without a long-lived npm token. If a token is still needed, it should not be a universal publish key.
For a trust relationship, verify that CI has stage-only permission. Then even a compromised job can only create a staged version, not publish it immediately.
4. Inspect non-registry dependencies
Find dependencies outside the registry:
node -e "const p=require('./package.json'); for (const [k,v] of Object.entries({...p.dependencies,...p.devDependencies})) if (/^(git\\+|https?:|file:|\\.\\.?\\/)/.test(v)) console.log(k, v)"
Start with a moderate policy:
npm install --allow-git=root --allow-remote=none --allow-file=root --allow-directory=root
root means “allow only what the root package explicitly declares”. That is a useful middle ground for teams with intentional local workspace patterns but no desire for surprise sources in the transitive dependency tree.
5. Add a CI smoke test
A separate job should verify install policy:
npm ci --allow-git=root --allow-remote=none --allow-file=root --allow-directory=root
If it fails, do not disable policy immediately. First identify which source was blocked and whether it is actually needed.
Anti-Patterns
- leaving
npm publishin CI with a broad token and calling it automation; - enabling staged publishing while CI still has full publish access;
- approving a staged release without inspecting package contents;
- setting
allow-remote=allbecause one old package needs it; - failing to pin Node.js and npm CLI in the release job;
- mixing publish hardening with a large release pipeline refactor.
Conclusion / Action Plan
A healthy minimum rollout looks like this:
- find every
npm publishpath; - upgrade the release job to Node.js 22.14.0+ and npm CLI 11.15.0+;
- move CI to
npm stage publish; - keep final approval with a human using 2FA;
- narrow CI to trusted publishing or stage-only permission;
- inspect package.json for non-registry sources;
- add
npm ciwith allow-* policy; - document exceptions and rollback.
Official sources:
Quick checklist
- Check whether CI has direct npm publish permission.
- Upgrade Node.js and npm CLI to versions that support staged publishing.
- Replace direct publish with npm stage publish.
- Keep final approval with a human using 2FA.
- Limit non-registry sources through allow-* controls.
- Document rollback for the release pipeline.
Prompt Pack: audit npm publish and install policy
Help audit npm supply-chain hardening in our Node.js project. Input data: - package.json and .npmrc; - how npm publish currently runs in CI/CD; - whether trusted publishing is used; - which npm tokens or trust relationships have publish permissions; - a package-lock.json snippet or list of non-registry dependencies; - current Node.js and npm CLI versions; - whether manual release approval exists. Return: 1. verdict: safe, risky, or critical publish-flow; 2. where direct CI publish exists; 3. migration plan to npm stage publish and manual approve; 4. recommended allow-git, allow-remote, allow-file, allow-directory values; 5. CI smoke test for install policy; 6. rollback plan if release or install breaks. Response format: verdict, risks, command diff, 30-minute rollout, rollback.