npm v12 Breaking Changes: What Every JavaScript Developer Needs to Know
npm 12.0.0 shipped on July 8, 2026, and was promoted to the latest tag on the same day. It is the most consequential npm major release since npm 7 overhauled the lockfile format. If you have not read the changelog yet, here is what you need to know: three npm install behaviors that have been automatic for a decade now require explicit approval. Several other breaking changes will silently break CI scripts and lockfile-driven build pipelines.
npm 12 is already bundled with Node.js 24.16.0 (released July 9) and Node.js 26.3.0 (released June 1), and the npm team has indicated it may be backported to both active LTS lines under the Node security policy. That means the new defaults reach the majority of Node users within weeks, not months.
The preview warnings have been available since npm 11.16.0 (May 27). If you did not run them yet, you are already behind.
The Three Security Default Changes
npm 12 turns three install-time behaviors from automatic into opt-in. Each one closes a code-execution path that real supply-chain attacks have used in the past two years.
Install Scripts Blocked by Default
npm install no longer runs preinstall, install, or postinstall scripts from dependency packages unless the package is on an explicit allowlist. This includes the implicit node-gyp rebuild that npm triggers for any package shipping a binding.gyp file, even when no explicit install script exists. prepare scripts from git, file, and link dependencies are blocked the same way.
Your own project scripts in the root package.json still run. If your postinstall runs patch-package or your prepare installs husky hooks, nothing changes for you. The block targets code arriving from the registry.
The allowlist lives in package.json under a new allowScripts field, managed by two new commands:
# See which packages have scripts that would be blocked
npm approve-scripts --allow-scripts-pending
# Approve specific packages
npm approve-scripts sharp esbuild
# Deny specific packages
npm deny-scripts node-sass
The resulting package.json entry looks like this:
{
"allowScripts": {
"sharp@0.33.5": true,
"esbuild@0.24.0": true
}
}
Approvals are pinned to specific versions by default. When you approve sharp@0.33.5, the approval does not automatically extend to sharp@0.34.0. You re-approve after reviewing the update. The --no-allow-scripts-pin flag bypasses this per-version pinning; use it only when you understand the trade-off.
The v12 default is a soft skip, not a hard failure. An unapproved script is skipped, npm prints a warning, and the install still succeeds with exit code 0. There is a separate opt-in setting, strict-allow-scripts, that turns the skip into a hard error for CI.
Git Dependencies Blocked by Default
npm install will not resolve git dependencies, direct or transitive, unless you set --allow-git. This closes a code-execution path where a transitive git dependency's .npmrc could override the git executable that npm invokes -- a path that remained open even under --ignore-scripts.
# Before (v11, still works but shows warning)
npm install github:some-org/private-fork#feature-branch
# After (v12, blocked)
# Error: git dependencies are not allowed by this project
# Fix: pass the flag
npm install github:some-org/private-fork#feature-branch --allow-git
# Or configure in .npmrc
echo "allow-git=true" >> .npmrc
The npm team explicitly advises against long-term reliance on the --allow-git escape hatch. For cross-package references within a monorepo, the workspace protocol (workspace:*) is the correct replacement. For external forks, publish a prerelease to your registry instead of pinning to a git SHA.
Remote URL Dependencies Blocked by Default
Dependencies pointing at HTTPS tarball URLs no longer resolve without --allow-remote:
{
"dependencies": {
"my-package": "https://artifactory.internal/example-pkg-1.0.0.tgz"
}
}
This produces a resolve error in v12 unless --allow-remote is set. The --allow-file and --allow-directory flags exist but their defaults are not changing in v12.
The primary migration path for these dependencies is to serve them through a proper npm registry (the public registry, a private npm registry, or a registry proxy like Verdaccio). npm's staged publishing feature, introduced in May 2026, adds a pre-publication review window for registry packages but does not cover ad-hoc tarballs.
Other Breaking Changes
The three security defaults dominate the conversation, but the v12 changelog also includes several breaking changes that will cause silent failures in CI pipelines.
npm shrinkwrap Removed Entirely
npm shrinkwrap is removed. npm-shrinkwrap.json is no longer loaded or honored at the project root or from inside dependency tarballs. The shrinkwrap config alias is also removed.
If you rely on npm-shrinkwrap.json for deterministic installs of published packages, you need to rename it:
mv npm-shrinkwrap.json package-lock.json
If you ship a locked dependency tree inside a published package (the original use case for shrinkwrap), migrate to bundleDependencies in package.json instead. The danger here is silent: a published package that ships only npm-shrinkwrap.json and no package-lock.json will have its pinned dependencies ignored by v12 consumers, who will resolve fresh against the registry.
npm view --json Always Returns an Array
In v11, npm view --json returned a single object for single-version packages and an array for multi-version packages. This inconsistency has been a footgun since it was introduced. In v12, the output is always an array:
# v11 behavior (single version)
npm view express@4.18.2 --json
# => { "name": "express", "version": "4.18.2", ... }
# v12 behavior (always array)
npm view express@4.18.2 --json
# => [{ "name": "express", "version": "4.18.2", ... }]
Any script that does JSON.parse(output).version without checking for an array will break on every call, not just the multi-version case. The fix is to always treat the result as an array:
// Before (breaks in v12)
const pkg = JSON.parse(output);
console.log(pkg.version);
// After (works in both)
const [pkg] = JSON.parse(output);
console.log(pkg.version);
npm pkg Output No Longer Forced to JSON
In v11, npm pkg get version returned a JSON-wrapped value: {"version":"1.2.3"}. In v12, single-value queries return a plain string:
# v11
npm pkg get version # => {"version":"1.2.3"}
# v12
npm pkg get version # => "1.2.3"
Scripts that pipe npm pkg through JSON.parse() and access .version will throw a TypeError. Unlike the npm view array change where you can add a destructuring wrapper, this one requires restructuring how the consuming code reads the output.
npm pack and npm publish JSON Output Reformatted
The --json output for both commands has been restructured in v12 to share a consistent schema. The changelog does not document the exact field-level diff, but the stated goal is unified output between the two commands. Any CI step that parses these outputs to extract tarball filenames, integrity hashes, or version numbers should be tested against v12 before deploying.
npm adduser Removed
The adduser command is gone. Create and manage user accounts on the npm website, and use npm login to authenticate on the command line. For CI pipelines, this is unlikely to affect you if you already use token-based authentication via .npmrc entries:
//registry.npmjs.org/:_authToken=${NPM_TOKEN}
Legacy provisioning scripts that call npm adduser will fail with a "command not found" error.
star, stars, and unstar Removed
Three commands for bookmarking packages on the registry are removed with no CLI replacement. If any CI script calls them, those calls will fail.
Man Pages No Longer Registered on Global Install
man npm-install and similar system man page lookups stop working. npm help install continues to work because it reads from npm's bundled documentation.
npm sbom CycloneDX Name Field Changed
npm sbom --sbom-format=cyclonedx now reports the name field from each package's package.json instead of the on-disk directory name. The name, bom-ref, and purl of the root component and of aliased dependencies may change. In monorepo setups where the directory is packages/logger/ but the package name is @scope/logger, every SBOM identifier will change. Compliance systems that key off bom-ref to track vulnerabilities across releases will see the same package appear as a new entry.
Default License Changed
npm init now defaults to an empty license string instead of "ISC". If not set, the license field is omitted from new packages.
whichnode Resolution Dropped
npm no longer resolves the Node binary path via PATH lookup. It relies solely on process.execPath. This is fine in most setups but can break when npm is invoked through a version manager or wrapper script that symlinks Node into the PATH.
root preinstall Runs Earlier
The root package.json preinstall script now runs before dependencies are installed, not after. If your root preinstall depends on any dependency being present in node_modules, it will fail.
Impact on CI/CD Pipelines
The three security changes affect CI pipelines differently depending on how your builds are configured.
If You Pin npm Version (Recommended)
If your Dockerfile or CI configuration pins npm to a specific major version:
RUN npm install -g npm@11
You control when v12 arrives. Schedule the upgrade like any other dependency bump: audit, test on a staging branch, then update the pin. This is the safest approach.
If You Float npm Version
If your Dockerfile does npm install -g npm@latest or you use a node:latest style tag, v12 arrives on its own schedule. On July 8, images rebuilt after the release will produce different install behavior with no code changes. This is how teams end up debugging a production incident on release day.
Breaking Changes in CI by Category
Lockfile resolution changes. If your CI runs npm ci and relies on npm-shrinkwrap.json, the entire install step will produce different dependency trees in v12. Rename to package-lock.json before upgrading.
JSON output parsing. Any CI step that captures output from npm view --json, npm pack --json, npm publish --json, or npm pkg get and parses it will produce wrong results until updated. This includes release automation, version bump scripts, and artifact publishing workflows.
Native addon compilation. Packages that compile C++ at install time (bcrypt, sharp in some configurations, older database drivers) will silently produce missing binary errors at runtime unless their scripts are on the allowlist. The install succeeds, the app crashes.
Git dependencies. Any monorepo or internal toolchain that references packages via git+https:// URLs will fail to resolve. This includes transitive dependencies: your top-level deps may be clean, but a dependency's own dependency could come from a git URL.
Downloaders. Packages whose postinstall fetches browser binaries (Puppeteer, Playwright, Cypress) will install the npm package but leave the binary behind. The failure mode is a runtime error about a missing executable, not an install error.
Migration Strategy
The entire v12 breaking change surface is available as warnings in npm 11.16.0 and later. You can audit and prepare without switching to v12.
Step 1: Upgrade to the Latest npm 11
npm install -g npm@11
npm --version # should be 11.16.0 or later
If you are on Node.js 24.16.0 or Node.js 26.3.0, this version is already bundled.
Step 2: Audit Scripts
npm approve-scripts --allow-scripts-pending
This lists every package with lifecycle scripts that would be blocked under v12 defaults. The output includes the command each script runs.
Ignore prepare scripts from registry packages on the first pass. prepare only executes when you install from git, file, or link -- never from a normal registry install. The audit lists them because it cannot know you will not switch a dependency to a git URL tomorrow, but for triage they are noise.
Focus on install and postinstall scripts. These are the ones that run on every clean install.
Step 3: Approve What You Trust
For each real script entry, decide:
- Approve it if the package's script is necessary and you trust the package. Run
npm approve-scripts <package>. - Test without it if you suspect the package works without the script. Many packages that ship native binaries via
postinstall(sharp, esbuild) already distribute prebuilt binaries throughoptionalDependencies-- the install script is a fallback for unsupported platforms. If your platform has a prebuilt binary, the script is unnecessary.
# Test whether a package works without its install script
npm deny-scripts sharp
npm ci
node -e "require('sharp')" # if this works, deny it permanently
Step 4: Commit the Allowlist
The allowScripts field is written to your package.json. Commit it. From now on, every new dependency that wants script execution shows up as a diff in code review instead of executing silently during install.
Step 5: Handle Git and Remote URL Dependencies
# Find all git dependencies in your tree
grep -r 'git+' package.json package-lock.json
# Find remote tarball dependencies
grep -r 'https://.*\.tgz' package.json package-lock.json
For each one, decide:
- Monorepo internal: switch to
workspace:*protocol. - Pre-release fork: publish a prerelease to your registry and depend on the version.
- Genuinely external: add
--allow-gitor--allow-remotein tightly scoped CI steps, and set a deadline to migrate.
Step 6: Audit CI Scripts
Check every CI pipeline that parses npm output:
# Find scripts that parse npm JSON output
grep -r 'npm.*--json' .github/ gitlab-ci.yml Jenkinsfile
Update parsing logic for the new output shapes. Test npm view --json and npm pkg get against v12 pre-release to confirm your parsers still work.
Step 7: Rename npm-shrinkwrap.json
# Check if you have one
find . -name "npm-shrinkwrap.json"
# If found, rename
mv npm-shrinkwrap.json package-lock.json
Step 8: Pin npm in CI
Add a specific npm version to your CI configuration:
# Before (floats)
RUN npm install -g npm@latest
# After (pinned)
RUN npm install -g npm@12
If you are not ready for v12, pin to npm@11 and schedule the v12 upgrade separately.
Step 9: Test Everything
# Run with v12 defaults (on npm 11.16+, warnings only)
npm ci
# Verify native modules load correctly
node -e "require('sharp'); require('esbuild'); console.log('OK')"
# Run the full test suite
npm test
Run this on a staging branch before touching your production CI configuration.
Monorepo Considerations
Monorepos have an additional layer of complexity in v12.
Workspaces are independent install scopes. Approving sharp at the monorepo root does not approve it in packages/api. You must run npm approve-scripts in each workspace that depends on a package with lifecycle scripts.
Git dependencies are common in monorepo setups for pre-release cross-references. The workspace protocol (workspace:*) is the correct replacement -- it resolves locally without fetching from git and is not affected by the --allow-git default.
Root .npmrc configuration does not cascade to workspaces. If you set allow-git=true in the root .npmrc, it affects root-level operations only. Each workspace resolves its own effective config. Either set the config in each workspace's .npmrc, or pass the flag at install time: npm ci --workspaces --allow-git.
Historic Context: How This Compares to Past Major Transitions
npm 7 (October 2020) was the last major that changed how installs fundamentally worked. It introduced the v2 lockfile format, automatic peer dependency installation, and workspace support. The transition took months -- teams that pinned npm version survived without disruption, teams that floated npm bumped into CI failures and spent days updating lockfiles.
npm 9 (February 2023) was relatively quiet: it removed npm audit --audit-level, changed npm pack output behavior, and tightened config validation. Most teams upgraded without incident.
npm 10 (June 2023) removed npm link step behavior changes and the --global style flag, again low-impact for most.
npm 12 is closer to npm 7 in blast radius. The security defaults affect every install, and the JSON output changes affect every CI pipeline that extracts metadata from npm commands. The preview period (warnings available since npm 11.16.0 on May 27) is short compared to the npm 7 transition, where the v2 lockfile warnings appeared months before the breaking switch. The compressed timeline reflects the urgency of the supply-chain attack wave: Miasma, the TanStack breach, and the Axios hijack all exploited the exact entry points that npm 12 closes.
pnpm blocked lifecycle scripts by default in version 10. Bun and Deno do not run lifecycle scripts at all for dependencies. npm 12 brings the default behavior in line with the rest of the ecosystem -- a decade late but finally here.
What the Community Is Saying
The community discussion on the Node.js release issue and the GitHub blog post covers the predictable range: security teams applauding, maintainers of native modules scrambling to update guidance, and platform teams asking about the transition story for monorepos.
One concrete concern that came up during the prerelease period: the announcement originally included a change that turned unknown configuration keys in .npmrc into hard EUNKNOWNCONFIG errors. This was reverted in PR #9729 -- unknown file configs now warn by default, with an opt-in strict-npmrc setting for anyone who wants the stricter check. Unknown CLI flags still error. The revert removed a real barrier for teams carrying legacy or tool-specific keys in their config, which was exactly the kind of friction that stalls a security upgrade.
Another gap the community has identified: npm approve-scripts --allow-scripts-pending lists which packages have scripts and the command each one runs, but not what those scripts do once they execute -- what files they pull in, whether they make network calls, or write outside the package directory. An RFC is under discussion proposing a review-report mode that surfaces those signals.
Timeline
| Date | Event |
|---|---|
| 18 February 2026 | --allow-git warning available in npm 11.10.0 |
| 20 May 2026 | npm 12.0.0-pre.0 (first prerelease) |
| 27 May 2026 | npm 11.16.0 (full warning set for all v12 breaking changes) |
| 1 June 2026 | Node.js 26.3.0 (bundles npm 11) |
| 9 June 2026 | GitHub blog post announcing v12 breaking changes |
| 19 June 2026 | npm 12.0.0-pre.1 (allow-git/allow-remote defaults harden) |
| 8 July 2026 | npm 12.0.0 final -- promoted to latest |
| 9 July 2026 | Node.js 24.16.0 (bundles npm 12) |
| Q3 2026 | 2FA-bypass token deprecation Phase 1 (block sensitive operations) |
| ~January 2027 | 2FA-bypass token deprecation Phase 2 (block direct publishing) |
npm is also beginning to deprecate granular access tokens configured to bypass two-factor authentication. Phase 1, expected in August 2026, blocks these tokens from performing sensitive account and publishing operations. Phase 2, around January 2027, removes their direct publishing capability. GitHub's guidance is to move automated publishing to trusted publishing (OIDC) or staged publishing with a human approval step.
What to Do This Week
- Upgrade to npm 11.16.0+ on one representative machine or CI runner.
- Run
npm approve-scripts --allow-scripts-pendingin each repo. Ignoreprepareentries on the first pass. - For each real install/postinstall entry, approve the ones you trust, test and deny the ones that work without.
- Commit the resulting
allowScriptsblock inpackage.json. - Grep for git and URL dependencies in
package.jsonandpackage-lock.json. Plan migration to workspace protocol or registry versions. - Audit CI JSON parsers against the new
npm view --jsonandnpm pkgoutput shapes. - Pin npm version in CI to either
npm@11(defer) ornpm@12(upgrade now).
The whole audit takes under an hour for a typical repository. Doing it now means July is a controlled migration instead of an incident investigation.
Related Articles
- Miasma Supply Chain Worm: Full Technical Breakdown -- Miasma exploited the exact install-time code execution paths that npm 12's
allowScriptsdefault now closes. The Phantom Gyp technique that drove the worm's propagation relied onbinding.gypfiles running implicitnode-gyp rebuildduring install -- the primary attack vector that npm 12 blocks by default.