By the end of this you will have an explicit allowScripts allowlist committed to package.json and a CI gate that fails the second a new unreviewed install script lands. When npm 12 flips the defaults in July 2026, your installs run exactly what they run today, because you already told npm what is allowed.
On June 9, 2026, the npm team published the breaking changes for v12, estimated for July 2026 (GitHub Changelog). npm install flips from "run every dependency's install script" to "run nothing unless you said so." allowScripts defaults to off, --allow-git defaults to none, and --allow-remote defaults to none.
The trap, and the reason this needs more than a one-liner: in npm 11.16.0 the new behavior ships as warnings only. Your install still runs every script, CI still goes green, nothing looks wrong. The day your runner pulls npm 12, native builds (node-gyp, better-sqlite3, bcrypt) stop executing and you get a module that fails deep in a request at runtime, not a loud failure at npm ci. The fix is to spend the 11.16 warning window writing an allowlist and committing it, so v12 is a no-op for you.
Prerequisites
- A project with a
package-lock.jsonand at least one dependency that runs an install script. Most native addons qualify:esbuild,better-sqlite3,bcrypt,sharp. - Ability to upgrade the npm CLI to 11.16.0 or newer. Node itself is unaffected; this is purely a CLI change.
- Write access to the repo and the CI config.
- Five minutes to actually read the warnings instead of scrolling past them.
Step-by-step
1. Upgrade the npm CLI to 11.16.0 or newer
npm install -g npm@latest
npm --version # must be >= 11.16.0
The three warning streams landed in stages: git-dependency warnings in npm 11.10.0, remote-URL warnings in 11.15.0, and install-script warnings in 11.16.0. You want 11.16.0+ so all three are visible at once. Everything below depends on having these warnings turned on.
2. List every package that wants to run a script
npm approve-scripts --allow-scripts-pending
--allow-scripts-pending is read-only. It prints every package whose install scripts are not yet covered by allowScripts and writes nothing to disk (npm docs). This is your inventory. Treat the output as a list you consciously approve or deny, not a nuisance to clear.
3. Approve the packages you actually trust
npm approve-scripts esbuild better-sqlite3
This writes pinned entries to a new allowScripts field in package.json. The field is a Record<PackageName, boolean>, and by default approvals pin to the exact installed version:
{
"allowScripts": {
"[email protected]": true,
"[email protected]": true
}
}
Pinning narrows the approval to the version you reviewed. Bump esbuild and the new version shows up as pending again until you re-approve it. That is the point: an attacker who ships a malicious [email protected] does not inherit your trust. To approve everything pending at once use npm approve-scripts --all, but only after you have read the list.
4. Deny the rest explicitly
npm deny-scripts left-pad some-analytics-sdk
npm deny-scripts always writes name-only entries, never pinned, regardless of your pin setting (npm docs):
{
"allowScripts": {
"[email protected]": true,
"left-pad": false,
"some-analytics-sdk": false
}
}
The asymmetry is deliberate. Pinning a deny to one version would silently re-allow scripts for every other version of that package, which defeats the purpose. approve-scripts will never overwrite an existing false entry, so your denials are sticky.
5. Choose your pinning strategy on purpose
# default: pin to the reviewed version (recommended for production deps)
npm approve-scripts better-sqlite3
# name-only: trust any future version (use sparingly)
npm approve-scripts --no-allow-scripts-pin typescript
--no-allow-scripts-pin writes "typescript": true and stops asking on every bump. Reserve it for packages you update constantly and trust fully. Rule of thumb: if a package runs a native compile step, keep it pinned. The version is exactly where a malicious postinstall would hide.
6. Audit for git and remote dependencies now
# these warn today, hard-fail in v12 unless allowed
npm install
# look for: "will not resolve Git dependency ... in v12"
In v12, a package.json entry like "dep": "github:org/repo" or a direct https:// tarball stops resolving unless you pass --allow-git or --allow-remote. The git block also closes a real code-execution path: a git dependency's own .npmrc could override the git executable even with --ignore-scripts set. If you find these in your tree, the durable fix is to publish those deps to a registry, not to sprinkle --allow-git across CI.
7. Commit the allowlist and gate CI on it
git add package.json
git commit -m "Add npm allowScripts allowlist for v12"
Then add a gate that fails the build if anything is pending:
# fails the build if any package's scripts are unreviewed
test -z "$(npm approve-scripts --allow-scripts-pending)"
This is the move that makes v12 a non-event. New dependency with a postinstall? The gate goes red in the PR, a human reviews it, and it never reaches the default branch unreviewed.
Verify it works
Run the read-only check. A clean project prints nothing:
npm approve-scripts --allow-scripts-pending
# (no output = every script-running package is reviewed)
Then confirm a fresh install is clean:
rm -rf node_modules
npm ci
# no "unreviewed install scripts" warnings
If both are quiet, your package.json already encodes v12 behavior, and the upgrade will not change what runs.
Common pitfalls
Global installs hit a catch-22. On npm i -g, the warning still tells you to run npm approve-scripts, but that command errors with EGLOBAL: it only works on project-scoped installs (npm/cli#9463). For global tools and npx, you cannot build an allowlist. You approve at install time with the install-level flags instead. Do not burn time trying to make approve-scripts work globally; it is a known gap.
ignore-scripts=true in .npmrc silently disables the whole system. If your project or CI sets ignore-scripts=true, the allowScripts field and its tooling are ignored entirely. You will think you are covered while the field does nothing. Pick one model: drop ignore-scripts and let allowScripts be the source of truth, rather than running both.
Native builds fail at runtime, not install. node-gyp compile steps run as install scripts. When v12 blocks them, the package installs but the compiled binary is missing, so you get a MODULE_NOT_FOUND or a binding error deep in a request, not at npm ci. Approve every native addon in step 3 explicitly.
npm-shrinkwrap.json stops working. v12 removes npm shrinkwrap, removes the shrinkwrap config alias, and no longer honors npm-shrinkwrap.json at the project root or inside dependency tarballs. If you ship one, migrate to package-lock.json before upgrading.
Pin churn feels noisy. Pinned approvals re-trigger as pending on every version bump. That is working as designed. If a specific dev dependency updates weekly and you trust it, move just that one to --no-allow-scripts-pin.
Wrap-up
You now have a version-pinned allowScripts allowlist in package.json, explicit denials for the packages you do not trust, an audit of any git or remote dependencies, and a CI gate that blocks new unreviewed scripts. When npm 12 lands in July 2026 and flips the defaults, your installs behave exactly as they do today.
Sensible next step: run npm approve-scripts --allow-scripts-pending across your other repos this week, while you are still in the 11.16 warning window and a miss costs you a warning instead of a broken production build.
Sources
- https://github.blog/changelog/2026-06-09-upcoming-breaking-changes-for-npm-v12/
- https://docs.npmjs.com/cli/v11/commands/npm-approve-scripts/
- https://docs.npmjs.com/cli/v11/commands/npm-deny-scripts/
- https://github.com/npm/cli/issues/9463


Comments
Be the first to comment.