npm v12 Disabled Install Scripts by Default and Your CI Is Already Quietly Broken
npm v12 shipped July 8 and made install scripts opt-in. If you haven't run npm approve-scripts yet, your CI is silently skipping native module builds — and exiting with code 0 the whole time.

npm v12 shipped July 8, 2026, and it brought a breaking change that is still catching teams off guard six weeks later: **install scripts are now off by default**. No more automatic preinstall, install, or postinstall execution during npm install. You have to explicitly approve them first.
If your local setup still works, it is probably because your machine still has npm 11.x pinned somewhere. Your CI pipeline might be a different story.
Why This Happened
Install-time scripts have been the entry point for some of the most damaging JavaScript supply chain attacks of the last few years. Malicious packages abuse postinstall to steal credentials, exfiltrate environment variables, fingerprint developer machines, and download secondary payloads — all silently, before you have had a chance to audit anything.
The attack is trivially easy. Publish a typo-squatted package with a postinstall that curls your attacker-controlled endpoint with process.env dumped into the body, and you are done. Thousands of CI runners have been compromised this way. The stolen goods are usually NPM_TOKEN, AWS_ACCESS_KEY_ID, and whatever else happens to be in the environment.
npm v12 flips the model. Scripts do not run unless you have reviewed and approved them. It is opt-in now.
What the New Flow Looks Like
Before v12, npm install would fetch a package and immediately execute any declared lifecycle scripts. There was no pause, no prompt, no record that it happened.
Before v12, that blocked step did not exist. npm would just run whatever the package declared. Now it stops, tells you what it found, and waits for explicit sign-off.
What Actually Breaks
The obvious cases are packages with explicit lifecycle scripts. But there is a less obvious one that catches almost everyone: **native modules**.
If a package has a binding.gyp file but no explicit install script in package.json, npm v12 still blocks the implicit node-gyp rebuild that used to run automatically. The package looks innocent in the manifest, but the native addon never gets compiled.
Common packages affected:
sharp— postinstall downloads platform-specific libvips binariesesbuild— postinstall fetches the correct platform binarybetter-sqlite3— native binding compiled via node-gypcanvas— cairo bindingspuppeteer— postinstall downloads Chromiumbcrypt— native crypto addon
The worst part: npm install exits with code 0 when scripts are blocked. The install succeeds. The crash happens later at runtime when the module tries to load a shared library that was never built. If you are not watching error logs closely, you will blame your application code before you think to blame the install step.
The Approval Workflow
Migrating is three commands:
# See what needs approval (dry run, no changes made)
npm approve-scripts --allow-scripts-pending
# Review the list, approve the ones you trust
npm approve-scripts
# Commit the resulting allowlist
git add package.json && git commit -m "chore: approve npm install scripts"The allowlist lives in package.json under a new allowedScripts key:
{
"allowedScripts": {
"sharp": ["postinstall"],
"esbuild": ["postinstall"],
"better-sqlite3": ["install"]
}
}Once committed, CI picks it up automatically and runs the approved scripts as part of normal install. You never have to re-approve a package unless its script changes.
The Approval State Machine
Each package with scripts moves through this flow:
That last edge is the important one. If a package updates its install script between versions — even a minor version bump — its approval is revoked automatically. You get the blocking behavior again on next install. That is intentional: a script change is a trust boundary change, and silent re-approval would defeat the whole point.
Fixing CI Environments
Most CI systems cache node_modules and only re-run install on lockfile changes. If you upgraded to npm v12 without running npm approve-scripts locally first, your cached modules are probably fine — but a cache miss will break your build the next time it happens.
For GitHub Actions, the temporary escape hatch is:
- name: Install dependencies
run: npm ci
env:
npm_config_allow_scripts: allDo not use npm_config_allow_scripts=all as a permanent fix. It turns off the entire safety net and puts you back to npm 11 behavior. Use it for one day while you run npm approve-scripts locally and commit the result, then remove it.
For monorepos using workspaces, run npm approve-scripts from the repo root. It traverses workspaces automatically and builds a single top-level allowlist.
Checking What Is Blocked Without Changing Anything
npm approve-scripts --allow-scripts-pendingThis prints the full list of packages with pending scripts and exits without writing anything. Run this first. In a typical 200-package dependency tree, you will usually see 3 to 8 packages. Review that list: if you recognize all of them, run npm approve-scripts for real.
The Git Dependency Trap
There is a second breaking change in v12 that is getting less coverage: --allow-git now defaults to none.
Any dependency installed from a GitHub URL like "foo": "github:org/foo#main" is now blocked by default. Same treatment, same fix.
# Install once with the flag
npm install --allow-git github:org/foo
# Or add to .npmrc for persistent permission
allow-git=github:org/fooIf your team has been sneaking in git dependencies for in-progress internal packages, this is where you find out. The failure mode is the same: exit 0, runtime crash.
Bottom Line
npm v12's install script blocking is the right call, even if the rollout has been painful. Supply chain attacks via lifecycle scripts have been a known, trivially exploitable vector for years. Requiring explicit approval before arbitrary code runs during install is basic hygiene that should have been the default long ago.
The migration is genuinely easy — one command, one commit. The annoying part is that npm install still exits 0 when scripts are blocked, so the failure mode is silent until something actually tries to use the unbuilt native module. Run npm approve-scripts --allow-scripts-pending right now, see what comes up, and commit the allowlist before your next cache miss turns into an incident.