The allowlist
you actually need.
npm v12 turned an automatic permission into a list you write by hand — and under deadline, almost everyone will write back the same permission, with a file in git to prove they meant it this time.
The build goes red on a deploy that was supposed to be routine, and the message is a shrug: some dependency's install script got skipped, try npm rebuild. Whoever is on call — not the person who reads dependency trees for fun, just whoever is holding the pager — has one job: make it green before standup. The fastest route is npm approve-scripts --all, which reproduces the default npm just spent a year removing, now committed to git as if it were a decision. It wasn't. It was a deadline.
What actually changed, and how quietly.
Three defaults flipped at once. allowScripts went from on to off, so a dependency's preinstall, install, postinstall, and — for git, file and link sources — prepare script no longer runs unless it has a matching entry in your package.json. --allow-git and --allow-remote both went from unrestricted to none, closing the same execution path when a dependency arrives as a git ref or a tarball URL instead of a registry package. None of it was a surprise: GitHub announced the change on 9 June 2026, and per the community's own breaking-changes thread, the flags had already shipped behind warnings — --allow-git in 11.10.0, --allow-remote in 11.15.0, the allowScripts field itself in 11.16.0. What changed in npm v12.0.0, published 8 July 2026, is which behaviour is the default.
The skip is quiet on purpose. Per npm's own reference for the command, an unapproved script is bypassed, a warning prints, and the install still exits clean — so a v12 upgrade doesn't turn every pipeline red the same morning. The gap that leaves: a package with a binding.gyp triggers an implicit native build with no explicit script to approve at all, and that build gets skipped the same quiet way. Nothing fails at install. It fails later, at request time, as a missing native binding — which reads like your code broke, not like a permission you never granted.
What an unreviewed install script can do isn't hypothetical. Microsoft's write-up of the ChainDrop campaign, published 4 August 2026, traces the payload to a preinstall script that launched a file called setup.mjs before installation even finished, across more than 400 packages spanning unrelated publishers — the worm then used stolen npm tokens to republish itself into every package each stolen identity could reach. That's the class of thing the old default let run on every npm install, on every machine, with nobody asked.
Four questions per pending package.
What does the script actually do?
Before approving anything, open the actual preinstall, install, or postinstall command — not the package name, the command itself. A native module compiling a binding for your platform reads differently from a script that fetches from a URL, writes outside node_modules, or runs unconditionally regardless of what platform it's on. In most trees, the packages that genuinely need this are a small, boring, recognisable handful; everything else is along for the ride.
Passes whenYou can state in one sentence what the script does and why the package can't work without it.
Is there already a compiled result?
Plenty of native-binding packages ship prebuilt binaries for the common platforms and fall back to compiling only when nothing matches — the install script's job there is to fetch, not to build. If your platform is already covered, or the package also ships a WASM or pure-JS build, the script has nothing load-bearing left to do on your machine.
Passes whenYou've checked for a prebuilt or fallback path before assuming the script is required.
Pin the approval to a version, not a name.
npm approve-scripts pins its entry to the version you just installed by default — written as "pkg@1.2.3": true — and only --no-allow-scripts-pin trades that for a standing grant to any future release under the same name. A version bump then reads as a new script to review, which is the point; npm deny-scripts gets the same discipline automatically, since its entries are always name-only.
Passes whenA dependency bump makes an existing approval stale again, not silently still valid.
Does ignore-scripts already sit in your config?
The older, blunter setting still wins over the one you just wrote. With ignore-scripts=true set anywhere in effect — root .npmrc, project .npmrc, a CI environment variable — no script runs regardless of what allowScripts says, and the allowlist you spent an afternoon curating does nothing at all. Check every level before trusting what's committed to git; the two were never meant to run together.
Passes whenExactly one of the two mechanisms is deciding, and you know which one it is.
Write the list before the build does it for you.
List what's pending, on an ordinary day
Run
npm install-scripts lsbefore anything is on fire. It surfaces every dependency npm is currently skipping, with no clock forcing a decision yet.Read before you approve
Open each pending script and read what it does, per the checks above. Approve only the ones with a genuine reason to run at install.
Approve narrow, deny explicitly
npm approve-scripts <pkg>for what earned it, pinned to the version installed.npm deny-scripts <pkg>for the rest, recorded by name so it stays denied across every future version. Thennpm rebuildto run what you approved — approval alone executes nothing.Make the failure loud, not silent
Set
strict-allow-scripts=trueso a missing approval halts the install before anything is written, instead of surfacing later as a broken native binding nobody connects to a skipped script.
This describes npm's own published defaults and command reference — not an audit of any dependency tree but yours. Deciding what belongs on the list is a call only the team that owns the build can make; with your permission, that review is part of what OOPUO's infrastructure hardening work looks at.
Then look at what else runs unreviewed.
An install-script allowlist is one line item in a build pipeline that runs a lot of code nobody has read recently — CI jobs, base images, the rest of the dependency tree behind it. That's the scope of infrastructure hardening: what runs, on whose authority, and what happens if it shouldn't have. Send what your pipeline currently trusts by default.