npm v12 Breaking Changes: Migration Guide for Your Project
For 12 years, npm let any package run arbitrary code during npm install.
The Shai Halud supply-chain attack proved this is no longer acceptable.
npm v12 flips the defaults: scripts off, Git deps blocked, remote URLs blocked — shipping July 2026.
Prepare now — npm 11.16.0+ already surfaces v12 warnings. 6 breaking changes ranked P0→P2 with copy-paste fixes.
What Is npm v12
npm v12 is a security-first major release that changes how npm install works.
Instead of auto-running dependency scripts, auto-resolving Git dependencies, and auto-downloading remote tarballs,
v12 makes you explicitly opt in to each of these behaviors.
For most developers this means: your npm install will stop running postinstall scripts by default.
Native modules (anything with node-gyp) won't compile.
Git dependencies in your package.json will fail to resolve.
Remote URL dependencies will block.
And npm-shrinkwrap.json is gone.
The release is estimated for July 2026. You can prepare today using npm 11.16.0+, which already shows warnings for the upcoming v12 behavior.
Why npm v12 Matters
1. Postinstall scripts are a supply-chain disaster — and v12 finally fixes them
npm has let any package in your dependency tree run arbitrary code during install for over a decade.
A single compromised package with a postinstall script can exfiltrate env vars, SSH keys, and .npmrc tokens.
The Shai Halud supply-chain attack made it impossible to ignore.
v12 flips the default: scripts are off. You approve them explicitly.
2. You will get v12 whether you want it or not
Node.js bundles npm. When LTS releases (22, 24, 26) pick up the v12 bundle, your CI pipelines and dev environments will suddenly have different npm behavior. The question isn't "should I upgrade?" — it's "when will it hit me and how do I prepare?"
3. Git and remote dependency blocking close two real code-execution vectors
A Git dependency's .npmrc could override the Git executable path, even with --ignore-scripts.
Remote tarball URLs are unverifiable. v12 closes both by requiring --allow-git and --allow-remote flags.
These are not theoretical — they're explicitly cited attack paths in the npm security announcements.
Breaking Changes — What Breaks and How to Fix It
P0 allowScripts defaults to off
What changed: npm install no longer runs preinstall/install/postinstall scripts, including implicit node-gyp rebuilds for native packages (those with binding.gyp and no explicit install script).
Who it affects: Any project with native dependencies — bcrypt, sharp, esbuild, Puppeteer, node-sass. This will break most projects.
The fix:
# Step 1: See what will be blocked npm approve-scripts --allow-scripts-pending # Step 2: Approve packages you trust npm approve-scripts bcrypt sharp node-gyp # Step 3: Block the rest npm deny-scripts # This writes an allowlist to package.json. Commit it.
For global installs, use .npmrc: allow-scripts=path/to/global-allowlist.json or run with --allow-scripts.
P1 --allow-git defaults to none
What changed: Git dependencies (direct or transitive) no longer resolve without --allow-git.
Who it affects: You if you have any git+https:// or github:user/repo dependencies in package.json, or any transitive dependency that does. Monorepo setups often use Git deps for pre-release versions.
The fix:
# Option A: per-command flag npm install --allow-git # Option B: npmrc (project or global) echo "allow-git=true" >> .npmrc # Option C: migrate to npm registry versions wherever possible
This change was pre-announced Feb 2026. Available as a warning in npm 11.10.0+.
P2 --allow-remote defaults to none
What changed: Dependencies from remote URLs (https tarballs, direct and transitive) no longer resolve without --allow-remote.
Who it affects: You if your package.json or any dependency points to https://example.com/package.tgz. Also affects some private registry setups that serve tarballs via raw URLs.
The fix:
npm install --allow-remote # Or in .npmrc: # allow-remote=true
The initial implementation had a bug (GitHub Issue #9347) where allow-remote=none blocked even standard registry tarballs. This was fixed — allow-remote correctly only blocks non-registry URLs now.
NO-OP --allow-file and --allow-directory are NOT changing
Local file/directory deps ("package": "file:../local-package") still work. Only git and remote are affected. This is the noise you can safely skip.
P1 npm-shrinkwrap.json is gone
What changed: npm shrinkwrap command removed, npm-shrinkwrap.json no longer loaded or honored at project root or inside dependency tarballs. The shrinkwrap config alias is also removed.
Who it affects: Legacy projects that still use npm-shrinkwrap.json instead of package-lock.json. Common in deployed CLI tools and daemons that publish to the registry.
The fix:
# Rename at project root mv npm-shrinkwrap.json package-lock.json # If you were shipping a locked dependency tree inside a published package, # use bundleDependencies instead: # In package.json: "bundleDependencies": ["dep-name"]
Note: If both files exist, npm previously preferred shrinkwrap. Now only package-lock.json works.
P1 npm view --json now always returns an array
What changed: Previously npm view package --json returned a single object for a single version. Now it always returns an array.
Who it affects: Any CI scripts, build tools, or automation that parsed single-object JSON from npm view.
The fix:
# Before: npm view lodash version --json → "4.17.21" # After: npm view lodash version --json → ["4.17.21"] # Fix your scripts to handle arrays: # jq example: was 'jq -r .version', now 'jq -r ".[0]"'
Other v12 changes P2 — unlikely to affect you
npm star/stars/unstarcommands removed (package favoriting feature)npm adduserremoved — usenpm login; create accounts on npmjs.comnpm pkgoutput no longer forced to JSON- Twitter and Freenode profile fields removed from registry
- Man pages no longer registered globally; use
npm helpinstead - SBOM CycloneDX format now reports
package.jsonname, not directory name npm uis now an alias fornpm update
Migration Checklist
- Upgrade to npm 11.16.0+ first —
npm install -g npm@11 - Run an install and review warnings —
npm installnow previews v12 behavior - Audit dependency scripts —
npm approve-scripts --allow-scripts-pendingshows every package that runs scripts - Approve trusted packages —
npm approve-scripts <package-name>and commit the updatedpackage.json - Check for npm-shrinkwrap.json —
ls npm-shrinkwrap.json— if found, rename topackage-lock.json - Find Git dependencies —
grep -r "git+" package.json— decide to allow or migrate to registry versions - Find remote URL deps —
grep -r "https://.*\.tgz" package.json— migrate or add--allow-remote - Fix npm view JSON parsing — grep your CI scripts for
npm view+--json— update to handle array output - Update .npmrc if needed — add
allow-git=trueorallow-remote=trueonly if you actually need them
npm install -g npm@11 # stays on v11 behavior
This is temporary — once Node.js bundles v12, rollback gets harder. Prepare now.
Who Gets Hit Hardest
CI/CD Pipeline Maintainer
Your CI runs npm ci in a clean environment. v12 means no scripts run by default — native modules won't compile, and your build will fail 30 seconds in with "cannot find module X.node". Fix: commit the approved-scripts allowlist, or add --allow-scripts to CI after auditing all deps.
Monorepo Maintainer
Workspaces, cross-references, and Git dependencies for pre-release packages are common in monorepos. --allow-git will be needed across the entire tree if any package has a Git dep. Check your root and all sub-package package.json files.
Package Author with shrinkwrap
If you publish a CLI or daemon and ship npm-shrinkwrap.json to lock deps, you need to switch to bundleDependencies or accept that package-lock.json won't ship with the published package (it's always ignored on publish). This is the hardest case — bundleDependencies has different semantics from shrinkwrap.
Everyday Developer — "npm install just stopped working"
You clone a repo, run npm install, and get errors about native modules or missing dependencies. Top 3 fixes: (1) npm install --allow-scripts, (2) check that the project has allow-scripts configured in package.json, (3) if it's an old project, there may be a npm-shrinkwrap.json that needs renaming.
FAQ
When exactly does npm v12 release, and when will it hit my Node.js?
npm v12 is estimated July 2026. No confirmed date for Node.js bundling. Current LTS lines (22, 24, 26) ship npm 11.x. Historically, new npm majors take 2–4 Node releases to land. But you should prepare now — v11.16.0 already shows warnings.
Will --allow-scripts give me the old behavior back?
Yes. Adding allow-scripts=all to .npmrc restores the pre-v12 behavior. But the point is to move to an explicit allowlist approach. npm approve-scripts per package is the intended workflow — it's more secure and transparent.
I have 200 dependencies. Do I need to manually approve every script?
No. Most packages don't have lifecycle scripts. Run npm approve-scripts --allow-scripts-pending to see the actual list — likely 3–8 packages. Approve the ones you trust (sharp, esbuild, node-gyp itself) and deny the rest.
What about Yarn and pnpm? Do they have the same issues?
pnpm already supports per-package script allowlisting (via pnpm.onlyBuiltDependencies). Yarn 4+ has similar supply-chain protections. Both have had these controls for longer than npm. If you're already on pnpm/Yarn, you may not feel this pain at all.
The allow-remote flag was blocking registry tarballs — is that fixed?
Yes. GitHub Issue #9347 tracked a bug where allow-remote=none blocked even standard npm registry downloads. Fixed in npm 11.15.0+. The flag correctly only blocks non-registry URLs now.
What if I still need npm-shrinkwrap.json for my published CLI tool?
Switch to bundleDependencies. Add "bundleDependencies": ["dep-a", "dep-b"] to package.json. When published, those deps are bundled in the tarball and installed from the local copy. Different semantics from shrinkwrap but solves the same problem.
How do I check if my project has hidden Git or remote deps from transitive dependencies?
Run npm ls --all | grep -E "(git\+|https://.*\.tgz)" while on npm 11.16.0+. Warnings will flag any transitive Git or remote deps. You can also audit your lockfile: grep -r "git+" package-lock.json.
Why does npm install fail with "node-gyp rebuild failed" after upgrading to npm v12?
npm v12 disables all lifecycle scripts by default — including the implicit node-gyp rebuild that compiles native addons. When a package has a binding.gyp file, npm used to auto-run node-gyp. Now it's blocked. Fix: run npm approve-scripts --allow-scripts-pending to see which packages need native compilation, then npm approve-scripts <package-name> for each one you trust (e.g., bcrypt, sharp, node-gyp itself). Or add --allow-scripts as a quick workaround during migration.
Why do I get "cannot find module X.node" after npm install in v12?
This error means a native addon failed to compile because its install script was blocked. The .node binary is produced by node-gyp rebuild which no longer runs by default. Check npm approve-scripts --allow-scripts-pending — the package with the missing .node file (e.g., node-sass, bcrypt) needs script approval. Once approved, re-run npm install and the native binary will be built.
Why is my postinstall script blocked in npm v12?
npm v12 flips the default: all lifecycle scripts (preinstall, install, postinstall) are blocked by default. This is the intended security behavior — npm won't run arbitrary code from dependencies without your explicit approval. To approve specific packages, use npm approve-scripts <name>. The approved list is stored in your package.json under "scripts". Commit this file so your team and CI share the same allowlist.
Why is --allow-scripts not working in npm v12?
If npm install --allow-scripts doesn't seem to work, check two things: (1) You're running npm >= 11.11.0 — the --allow-scripts flag was added in that version and may behave differently in early releases. (2) In npm v12, --allow-scripts only enables the top-level install scripts, not transitive dependency scripts — you still need per-package approval for deep dependencies. Use npm approve-scripts --allow-scripts-pending to see the full list.
Why is npm-shrinkwrap.json ignored / not loaded in npm v12?
npm-shrinkwrap.json is fully removed in v12 — the file is no longer read, loaded, or honored, even if it exists at the project root or inside published tarballs. Rename it to package-lock.json: mv npm-shrinkwrap.json package-lock.json. If you were shipping shrinkwrap inside a published package to lock dependency trees, switch to bundleDependencies in package.json instead.
Why do Git dependencies fail with "git+https://" not resolving in npm v12?
npm v12 blocks all Git-based dependencies (git+https://, github:user/repo) by default. You'll see errors like npm ERR! Git dependency not allowed or the install simply skips them. Fix: either add --allow-git to your npm install command, set allow-git=true in .npmrc, or (preferred) migrate Git dependencies to registry-published versions. Check your tree with grep -r "git+" package.json package-lock.json.
Why does npm view --json return an array instead of an object in v12?
npm v12 changed npm view --json output to always return an array, even for single-version queries. Scripts that parsed the old single-object format will break — e.g., jq -r '.version' needs to become jq -r '.[0].version'. Search your CI scripts and Makefiles for npm view combined with --json and update the JSON parsing accordingly.
npm ci exits with code 0 but native modules weren't built — why is my deploy broken?
This is the most dangerous scenario in npm v12. npm ci doesn't fail when scripts are blocked — it just skips them and exits cleanly. Your CI passes, but node_modules is missing compiled .node binaries (bcrypt, sharp, etc.). The app crashes at runtime with "cannot find module X.node", not at build time. Fix: Always run npm approve-scripts --allow-scripts-pending in CI first, approve the required packages, and commit the allowlist. Add a smoke test after npm ci that actually requires the native module — don't rely on exit codes alone.
I approved scripts in my monorepo root but workspace packages still can't run install scripts
npm v12's allow-scripts approval is per-package and stored in each package's package.json. The root package.json allowlist doesn't propagate to workspace packages — each workspace with native deps needs its own npm approve-scripts run from within that workspace directory. For monorepos, the safest approach: set allow-scripts=all in the root .npmrc temporarily while you audit, then run npm approve-scripts --allow-scripts-pending in each workspace directory before committing individual allowlists.
I set allow-scripts=true in .npmrc but scripts are still blocked — what's the real precedence?
There's a critical distinction: allow-scripts=true in .npmrc enables script execution globally, but each package still needs to be in the allowlist stored in package.json. Think of it as a two-gate system: gate 1 is the global on/off switch (.npmrc / --allow-scripts flag / env var), gate 2 is the per-package allowlist. Both must pass. If you want the old "everything runs" behavior, set allow-scripts=all (not true) in .npmrc. The precedence order for the global switch is: CLI flag > environment variable > .npmrc > default (off).
npm rebuild doesn't compile native modules in v12 — I thought rebuild bypassed the block?
npm rebuild is also subject to script blocking in v12. If a package isn't in the allowlist, npm rebuild <package> will skip it silently. This breaks the classic troubleshooting workflow of "just run npm rebuild." Fix: approve the package first with npm approve-scripts <package>, then npm rebuild <package> will actually compile. Alternatively, npm rebuild --allow-scripts bypasses the block for the rebuild command only.