If a JavaScript project suddenly installs successfully but then fails because a native module was never built, a generated file is missing, or a dependency's postinstall step did not run, npm 12 may be the reason.
The change is deliberate. npm 12 flipped dependency install scripts from run by default to blocked unless approved. That includes preinstall, install, postinstall, and implicit node-gyp builds. The goal is to reduce the amount of third-party code that can execute automatically just because someone ran npm install.
Version check — August 12, 2026: npm's official v12 documentation and npm registry currently identify 12.0.1 as the latest CLI version. GitHub announced npm 12 as generally available and tagged
lateston July 8, 2026.
The quick answer
Do not fix an npm 12 migration by permanently turning every install script back on.
Instead:
- confirm the machine or CI runner is actually using npm 12;
- list dependencies whose scripts are not approved;
- review why each one needs install-time code execution;
- approve only the packages you trust;
- commit the resulting
allowScriptspolicy topackage.json; - rerun a clean install in CI.
The useful command to start with is:
npm approve-scripts --allow-scripts-pending
That command is read-only: it shows packages with install scripts that are not yet covered by the project's allowScripts policy.
Then approve specific packages, for example:
npm approve-scripts canvas sharp
npm writes the approvals to package.json. By default, approvals are pinned to the installed package version, which makes the permission narrower than approving every future version automatically.
What actually changed in npm 12?
npm has long allowed dependencies to run lifecycle scripts during installation. That is useful for legitimate jobs such as compiling native bindings, downloading platform-specific binaries, or generating files.
It is also a powerful execution path: a dependency can run code on a developer laptop or CI runner during an ordinary install.
npm 11.16.0 introduced warnings so projects could see which dependencies relied on this behaviour before the default changed. npm 12 completed the switch.
| Install behaviour | npm 11.16+ migration phase | npm 12 default |
|---|---|---|
Unreviewed dependency preinstall / install / postinstall | Warned, but scripts still ran by default | Scripts are blocked unless covered by allowScripts |
Implicit node-gyp build | Could run automatically | Blocked unless the dependency is approved |
| Git dependencies | Previously resolved by default | allow-git defaults to none |
| Remote URL dependencies such as HTTPS tarballs | Previously resolved by default | allow-remote defaults to none |
The important distinction is that blocked does not mean malicious. npm is no longer treating execution as implicit trust.
A dependency may have a perfectly legitimate install script and still be blocked until the project explicitly permits it.
Why can the install look successful while the app is broken?
The new policy normally skips an unapproved dependency script and reports what was blocked. That means the package itself can still appear inside node_modules, even though a setup step it depended on never happened.
This produces failures that can initially look unrelated to npm.
Common symptoms include:
- a native addon failing to load;
- a package complaining that a binary is missing;
- generated assets or bindings not appearing;
- a dependency working on an older laptop but failing in a newly rebuilt CI image;
- a Docker build changing behaviour after its Node/npm base image updates;
npm cifinishing, followed by application startup or tests failing later.
Native packages deserve particular attention. npm's package rules can implicitly use node-gyp rebuild when a package contains binding.gyp and does not define its own install or preinstall script. npm 12's install-script policy covers that path too.
A five-minute migration checklist
1. Confirm the npm version everywhere
Run:
npm --version
node --version
Do this locally and in CI. A common migration trap is testing with npm 11 locally while a fresh CI image has moved to npm 12.
If the failure appeared immediately after a base image, Node distribution, buildpack, or CI environment refresh, package-manager drift is worth checking before changing application code.
2. List the pending install scripts
Run:
npm approve-scripts --allow-scripts-pending
Do not interpret the list as a malware report. Treat it as a review queue: these packages want permission to execute code during installation.
3. Review each package before approving it
For every package in the list, answer three questions:
- Why does this dependency need an install-time script?
- Is it a dependency the project intentionally uses or an unexpected transitive package?
- Does the installed version and source match what the lockfile is expected to resolve?
A native image library compiling bindings is a different situation from an unfamiliar transitive dependency unexpectedly adding a postinstall script.
4. Approve narrowly
Approve the packages the project actually needs:
npm approve-scripts package-a package-b
The npm 12 docs say approvals are version-pinned by default, such as package-a@1.2.3. That means a future version does not automatically inherit permission simply because the package name is the same.
If a package should explicitly not be allowed to run install scripts, npm also provides:
npm deny-scripts package-name
A denial is written into the same allowScripts policy as false and is not silently undone by a later blanket approval.
5. Commit the policy and test from clean state
The allowScripts field is project policy, not a local preference. Commit the changed package.json, then test the same path CI uses:
rm -rf node_modules
npm ci
npm test
Use the project's real build and test commands as well. An install passing is not enough if the skipped script used to create something required at runtime.
Why --dangerously-allow-all-scripts is the wrong permanent fix
npm 12 includes an escape hatch named dangerously-allow-all-scripts. As the name suggests, it bypasses the allowScripts policy and lets every dependency install script execute.
That can be useful for a short migration experiment: if the project works with the bypass and fails without it, install-script blocking is a strong suspect.
But npm's own documentation says the option is intended as a migration escape hatch and strongly discourages its use as the normal configuration.
The reason is straightforward. Turning every script back on recreates the trust model npm 12 was designed to narrow.
A safer pattern is:
temporarily confirm the diagnosis → identify which scripts are required → approve those dependencies → remove the broad bypass.
There are two other npm 12 install changes worth checking
Install scripts are the most visible change, but npm 12 also tightened two dependency-source paths.
Git dependencies are opt-in
The allow-git setting now defaults to none in npm 12. A dependency pointing directly at a Git repository may therefore stop resolving unless the project explicitly allows that source.
This matters for package.json entries such as GitHub shorthand, Git URLs, or transitive packages that resolve from Git rather than a registry release.
The security rationale is different from lifecycle scripts: Git dependency resolution invokes Git against a remote repository and can introduce configuration outside the normal registry package path.
Arbitrary remote tarball URLs are opt-in
allow-remote also defaults to none in npm 12.
This does not mean ordinary npm registry packages stop working. npm's current documentation says tarballs hosted on the same hostname as the configured registry continue to install normally. The restriction is aimed at dependencies fetched from arbitrary remote URLs or registries whose tarballs come from another host without the appropriate configuration.
So if npm 12 fails before the lifecycle-script stage, inspect whether the project depends on Git repositories or external tarball URLs rather than assuming allowScripts is the only possible cause.
A simple diagnostic framework
When an npm 12 upgrade breaks an install, classify the failure before changing settings:
| Symptom | First thing to inspect | Likely control |
|---|---|---|
| Package installs, but native binary/generated output is missing | Blocked lifecycle-script warning | allowScripts / npm approve-scripts |
| Dependency points to a Git repo and cannot resolve | Dependency source in package.json or lockfile | allow-git |
| Dependency uses an external HTTPS tarball | Resolved URL and registry host | allow-remote |
| Team wants unreviewed scripts to fail CI instead of merely being blocked with a warning | CI policy | strict-allow-scripts |
| Everything works only when all scripts are globally re-enabled | Pending script list | Replace broad bypass with narrow approvals |
This prevents one of the easiest migration mistakes: weakening several security controls when only one dependency needed an explicit exception.
Should CI use stricter behaviour than a laptop?
Potentially, yes.
npm 12's strict-allow-scripts setting can turn an unreviewed install script from a blocked-with-warning condition into a hard installation failure. That can be useful in CI because a newly introduced dependency with an install script becomes an explicit code-review event rather than something developers notice later in logs.
One practical policy is:
- keep
allowScriptsunder version control; - review changes to it like dependency changes;
- enable stricter enforcement in CI once the migration is stable;
- require a reason when a new dependency needs install-time execution.
That turns the npm 12 change from a one-time compatibility annoyance into a small supply-chain control the team can maintain.
What to watch next
npm's security changes are broader than installation. GitHub's July 8 announcement also began deprecating sensitive uses of npm granular access tokens configured to bypass 2FA, with further publishing changes expected later.
For projects upgrading today, however, the immediate lesson is simpler: npm install is no longer supposed to mean “run whatever setup code every dependency asks for.”
The project now owns an explicit list of dependencies that deserve that execution permission.
Conclusion
If npm 12 breaks a previously healthy JavaScript build, check the package manager before rewriting application code.
The safest migration is not to restore the old global behaviour. List the blocked scripts, understand why each dependency needs them, approve only the required packages, commit that policy, and test from a clean environment.
Also check Git and external URL dependencies, because npm 12 made those source paths opt-in as well.
That takes a little more work during the upgrade, but it gives a project something the old default did not: a reviewable answer to the question “Which dependencies are allowed to execute code during installation?”
Sources
Checked August 12, 2026:
- GitHub Changelog — npm install-time security and GAT bypass2fa deprecation
- GitHub Changelog — Upcoming breaking changes for npm v12
- npm Docs —
npm approve-scriptsfor npm 12 - npm Docs —
npm deny-scriptsfor npm 12 - npm Docs — npm 12 configuration (
allow-git,allow-remote,allow-scripts,strict-allow-scripts) - npm Docs —
package.jsonbehaviour and implicitnode-gypinstall script
