In March 2026, a supply chain worm spread across 47 npm packages in under 72 hours. In April, the Mini Shai-Hulud campaign used stolen OIDC tokens to backdoor TanStack and Mistral AI packages before any human noticed. In both cases, the attack moved faster than any manual review could catch — because there was no manual review. Packages published instantly. That just changed.

TL;DR

  • npm packages no longer publish directly to the registry — they enter a staging queue first
  • A human maintainer must approve via 2FA before the package becomes installable
  • Available from npm CLI 11.15.0 with npm stage publish
  • Three new install-time flags let you block non-registry dependency sources
  • npm CLI v12 will make --allow-git strict by default

Why This Matters

If your project uses npm packages — and it almost certainly does — this change affects how attackers can weaponize the packages you depend on.

Supply chain attacks work by hijacking the publication step: a compromised CI/CD token, a stolen maintainer account, or a dependency confusion trick allows a malicious version to land in the registry before anyone reacts. From there, the next npm install in any downstream project picks up the poisoned package automatically.

Staged publishing breaks this loop by inserting a human checkpoint. An automated pipeline can’t silently push malicious code anymore — it needs a real person with a working second factor to approve.


The Old Problem: Publish Is Instant

The traditional npm publish command works like this:

  1. CI pipeline runs, builds the package
  2. npm publish uploads it directly to the registry
  3. Package is immediately available to every npm install worldwide

This is fast and convenient. It’s also exactly what attackers exploit. The Axios attack in March 2026 hit both release branches within 39 minutes and was downloaded by real users before the maintainers even noticed. The attack surface was the gap between “CI pipeline triggered” and “human reviews the artifact.”


The New Flow: Staged Publishing

GitHub’s staged publishing adds a queue between the pipeline and the registry.

CI pipeline → npm stage publish → [staging queue] → maintainer approves (2FA) → registry

Instead of going live immediately, the prebuilt tarball sits in a staging area. The package maintainer gets notified, reviews the release, and approves it by passing a 2FA challenge. Only then does the version become installable.

This means a compromised CI token, a stolen OIDC credential, or a malicious workflow can upload to the staging queue — but it cannot publish to the world without a human in the approval seat.

How to Enable It

Update to npm CLI 11.15.0 or newer:

Terminal window
npm install -g npm@latest

Then replace npm publish with npm stage publish in your CI configuration:

# GitHub Actions example
- name: Stage npm package
run: npm stage publish
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}

The maintainer then approves via the npmjs.com dashboard after passing a 2FA challenge.


Pairing With Trusted Publishing (OIDC)

Staged publishing works best when combined with trusted publishing — npm’s OIDC-based authentication that eliminates long-lived access tokens entirely.

With OIDC, your GitHub Actions workflow proves its identity to npm using a short-lived token signed by GitHub. No NPM_TOKEN secret needed. No credential that can be stolen from your repository settings.

The combination looks like this:

LayerWhat It Does
Trusted publishing (OIDC)Removes long-lived credentials from CI
Staged publishingBlocks automated live releases
2FA approval gateRequires human verification for every release

You can enforce this even harder with a “stage-only” policy on your npm package — this setting rejects standard npm publish commands entirely. Anyone (or any pipeline) that tries to bypass the staging queue gets blocked automatically.


New Install-Time Flags

Alongside staged publishing, npm 11.15.0 introduces four flags that control what dependency sources npm install will accept. Think of them as a firewall for your dependency graph.

FlagControls
--allow-fileLocal file paths and tarballs
--allow-remoteRemote HTTPS URLs
--allow-directoryLocal directory installs
--allow-gitGit repository installs

Each flag accepts either all (current default — everything allowed) or none (block completely). You set these in .npmrc or package.json.

Why This Matters for Defense

Non-registry install sources are a common supply chain attack vector. Dependency confusion attacks work by registering a public package with the same name as an internal package — the installer then pulls the attacker’s version from npm instead of the internal one. Git-based dependencies are another weak point: a compromised GitHub repo can push malicious code that gets pulled in at install time.

Locking these sources down is straightforward:

# .npmrc — block all non-registry sources except what you explicitly need
allow-git=none
allow-remote=none
allow-file=none

For most projects, registry packages are all you need. Blocking everything else removes entire categories of attack surface without any code changes.


What Changes in npm v12

The --allow-git flag currently defaults to all — meaning Git repository dependencies work out of the box. In npm CLI version 12, this default flips to none.

If your project has any git+https:// or github:user/repo entries in package.json, they will break on upgrade. Audit your dependency list now:

Terminal window
# Find git-sourced dependencies before v12 ships
grep -E '"git\+|"github:|"bitbucket:|"gitlab:' package.json package-lock.json

If you find any, replace them with versioned registry packages where possible. If you genuinely need a git source, you’ll need to explicitly set allow-git=all — but that’s a decision you’ll make consciously, not by accident.


What You Can Do Today

If you maintain an npm package:

  1. Update to npm CLI 11.15.0: npm install -g npm@latest
  2. Switch CI publish step from npm publish to npm stage publish
  3. Enable trusted publishing (OIDC) on npmjs.com to eliminate token secrets
  4. Enable the “stage-only” enforcement policy on your package

If you install npm packages:

  1. Add allow-git=none and allow-remote=none to your project .npmrc
  2. Run grep -E '"git\+|"github:' package.json to find any git dependencies
  3. Pin your dependencies to exact versions — ranges let attackers slide in new versions
  4. Consider running npm audit in CI as a blocking step

For teams:

  1. Review your CI/CD pipelines for long-lived npm tokens — replace with OIDC where possible
  2. Set up alerts for new package versions published by packages you depend on
  3. Test your install with strict flags enabled in a staging environment before rolling to production


Sources