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 latest on July 8, 2026.

The quick answer

Do not fix an npm 12 migration by permanently turning every install script back on.

Instead:

  1. confirm the machine or CI runner is actually using npm 12;
  2. list dependencies whose scripts are not approved;
  3. review why each one needs install-time code execution;
  4. approve only the packages you trust;
  5. commit the resulting allowScripts policy to package.json;
  6. 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 behaviournpm 11.16+ migration phasenpm 12 default
Unreviewed dependency preinstall / install / postinstallWarned, but scripts still ran by defaultScripts are blocked unless covered by allowScripts
Implicit node-gyp buildCould run automaticallyBlocked unless the dependency is approved
Git dependenciesPreviously resolved by defaultallow-git defaults to none
Remote URL dependencies such as HTTPS tarballsPreviously resolved by defaultallow-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 ci finishing, 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:

SymptomFirst thing to inspectLikely control
Package installs, but native binary/generated output is missingBlocked lifecycle-script warningallowScripts / npm approve-scripts
Dependency points to a Git repo and cannot resolveDependency source in package.json or lockfileallow-git
Dependency uses an external HTTPS tarballResolved URL and registry hostallow-remote
Team wants unreviewed scripts to fail CI instead of merely being blocked with a warningCI policystrict-allow-scripts
Everything works only when all scripts are globally re-enabledPending script listReplace 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:

  1. keep allowScripts under version control;
  2. review changes to it like dependency changes;
  3. enable stricter enforcement in CI once the migration is stable;
  4. 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:

Written and reviewed by /lico

Just writing down my thoughts, interests, and the things I learn along the way.