npm 12 migration

npm 12 turned off install scripts. Deciding what to turn back on is the hard part.

Updated 2 September 2026 npm 12.0.0 shipped 8 July 2026 ~10 minute read

What this covers

  1. What actually changed, and how it breaks
  2. Getting the list of pending scripts
  3. The setting that makes the list lie to you
  4. The only three questions that matter
  5. What the common packages actually do
  6. Answering all of it in one command
  7. Writing the allowlist: names or pinned versions
  8. Enforcing it in CI
  9. What none of this tells you

What actually changed

npm 12.0.0 shipped on 8 July 2026 and made install-time code execution opt-in. preinstall, install and postinstall from your dependencies no longer run unless the project explicitly allows them — and that includes the implicit node-gyp rebuild npm performs for any package with a binding.gyp. Git and remote-URL dependencies became opt-in in the same release, behind --allow-git and --allow-remote.

The part that catches teams out is how quietly it fails. An unapproved script is skipped, npm prints a warning, and the install succeeds. Your CI goes green. What breaks is whatever depended on the script having run: a native addon that never compiled, a browser binary that was never downloaded. That surfaces later, at runtime, in a request — usually as cannot find module ...node.

A green build is not evidence that this went well. Read the install warnings, or turn them into errors with strict-allow-scripts in CI, which is covered below.

Step 1: get the list

Install your dependencies first, so npm can see which of them declare scripts, then ask for the pending ones. This is read-only and changes nothing:

npm install
npm approve-scripts --allow-scripts-pending

You get back the packages waiting on a decision and the command each one would run. On a typical application that is somewhere between three and a dozen entries, and it is the easy half of the job.

Step 2: check the setting that makes the list lie

If your project or CI sets ignore-scripts=true in .npmrc — a very common hardening step from before npm 12 — it silently bypasses the entire allowScripts system. Nothing runs even when allowScripts is correctly filled in, and --allow-scripts-pending reports nothing pending, because from npm's point of view nothing is.

grep -r "ignore-scripts" .npmrc ~/.npmrc 2>/dev/null

If you find it, decide deliberately: keep ignore-scripts=true and accept that no native module will ever build, or remove it and let allowScripts do the job it was designed for. What you should not do is leave it in place and believe your allowlist is being enforced.

Step 3: the only three questions that matter

Every other guide stops at "review them carefully." Here is what reviewing carefully actually means. For each pending package, an install script is worth worrying about if it does any of three things:

  1. Reaches the network. Downloading a prebuilt binary is normal and also exactly what an attacker does. The question is not whether but from where: the vendor's own release host, or somewhere you have never heard of.
  2. Reads environment variables or credentials. There is no legitimate reason for an install script to read process.env wholesale, or to touch ~/.npmrc, ~/.ssh, or cloud credential files. In CI those variables are your deploy keys.
  3. Writes outside its own directory. A build step writes into the package. Writing to your home directory, a shell profile, or authorized_keys is persistence, not installation.

Everything else — compiling C++, checking whether a prebuilt binary already exists, printing a funding notice — is noise. If a script does none of the three, it does not deserve more of your attention than the thirty seconds it takes to confirm that.

What the common packages actually do

These are the eight packages most likely to appear in your pending list, reviewed at the versions current on 2 September 2026. No install script was executed to produce this table; each package's tarball was downloaded, verified against the registry's integrity hash, and read.

Package Install script Verdict What it means
bufferutil node-gyp-build approve Local compile only. Nothing to decide.
utf-8-validate node-gyp-build approve Local compile only. Nothing to decide.
esbuild node install.js review Fetches over HTTPS and shells out to npm install. The one package here where the evidence is unambiguous — see below.
better-sqlite3 prebuild-install || node-gyp rebuild review Reads like a compile, but the first branch downloads a prebuilt binary.
bcrypt node-pre-gyp install --fallback-to-build review Same shape: download first, compile only as a fallback.
sharp node install/check.js || npm run build review Checks for an existing binary, then builds. No suspicious pattern in its own code.
puppeteer node install.mjs review Downloads a browser, but through a dependency — nothing matches in puppeteer's own tarball. See the limits section.
@swc/core node postinstall.js review Nothing matched. Unrecognised rather than suspicious.

Two of eight are decidable automatically. That is the honest number, and it is the reason "just review them" is unhelpful advice: most real packages land in the middle, and what you need is not a verdict but the specific thing the script does, so that a thirty-second judgement replaces an hour of tarball archaeology.

Here is what that looks like for the one case with hard evidence:

REVIEW   esbuild@0.28.2
  postinstall: node install.js
  network_and_shell: Code combines network access with shell execution.
  Evidence install.js:147: function fetch(url) {
    ... https.get(url, (res) => {
  Evidence install.js:187: child_process.execSync(
    `npm install --loglevel=error ... ${pkg}@${packageJSON.version}`)

Now the decision is easy and it is yours: esbuild downloads its platform binary and shells out to npm to install it. That is documented, expected behaviour for esbuild, and you can approve it knowing exactly what you approved.

Answering all of it in one command

Reading eight tarballs by hand is the reason people rubber-stamp allowlists. This does the reading:

npx npx-vibe approve-scripts

It reads package-lock.json for every dependency npm would let run an install script, downloads and integrity-checks each tarball, reads the script and the files it reaches, and returns approve, review, or deny with the source line behind each one. Nothing is executed.

It reads the lockfile directly rather than shelling out to npm, so it works on npm 10 and 11 as well as 12 — you can settle the list before you upgrade, instead of after your CI starts warning.

Step 4: write the allowlist

npm records the decision in package.json as an object keyed by package name or by exact name@version:

{
  "allowScripts": {
    "bufferutil": true,
    "utf-8-validate": true,
    "esbuild": true,
    "some-package": false
  }
}

Prefer bare names. A pinned sharp@0.34.5 entry stops applying the moment you bump the dependency, and you will be re-approving on every upgrade. Pin only where you genuinely want a fresh decision each time.

To have the unambiguous decisions written for you:

npx npx-vibe approve-scripts --write

This writes only the packages it could decide: true for approve, false for deny. Anything needing review is deliberately left out. Auto-approving the ambiguous middle would recreate exactly the rubber stamp the allowlist exists to replace. Commit the result so your machine and your CI agree.

Step 5: enforce it in CI

On a developer machine a skipped script is a warning. In CI it should be a failure, or you will ship an application whose native modules never built:

npm ci --strict-allow-scripts

Do not turn that on locally. A newly added dependency with an install script would fail your install before you have had the chance to review it.

To catch an unreviewed script at the point it enters the lockfile — in the pull request, rather than in a deploy — add the review to your workflow:

- uses: Devrajsinh-Jhala/NPM-Vibe-check@v2
  with:
    command: approve-scripts
    fail-on: caution

With fail-on: caution the job fails when any dependency requests script execution that nobody has decided on yet. Annotations land on the pull request and a summary table on the job.

What none of this tells you

Being straight about the edges, because a security tool that oversells itself is worse than none:

Quick reference

TaskCommand
See what is pendingnpm approve-scripts --allow-scripts-pending
Find out what those scripts donpx npx-vibe approve-scripts
Write the decidable onesnpx npx-vibe approve-scripts --write
Approve one package, any versionnpm approve-scripts <pkg> --no-allow-scripts-pin
Build the native modules after approvingnpm rebuild <pkg>
Fail CI on a skipped scriptnpm ci --strict-allow-scripts
Check the trapgrep -r "ignore-scripts" .npmrc

Start with the list you actually have

No install script is executed, nothing is written unless you pass --write, and it works on npm 10, 11 and 12.

npx npx-vibe approve-scripts

Sources