TL;DR: This is a warning, not an error. Your install still runs today. In npm 12 (July 2026) it becomes a hard failure unless you approve each package. Runnpm approve-scripts --allow-scripts-pendingto see the full list, thennpm approve-scripts <pkg>for each one you trust. Commit the result topackage.jsonand you are done. Keep reading for the pitfalls that will bite you if you skip steps.
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 target is the same fast-publish supply-chain window that dependency cooldowns close from the release-timing side: install scripts are where a compromised package actually executes.
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. It is the same discipline as replacing static AWS keys with OIDC in GitHub Actions: standing trust out, explicit review in.
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.
If you are hardening the broader supply chain while you are at it, Dependency Cooldowns Beat Fast Supply-Chain Attacks covers the complementary control: gating how quickly new dependency versions can reach production. On the container side of the same supply chain, generate an SBOM with Syft and gate it with Grype so you know what actually shipped, pick the scanner that fits the job rather than the fastest one, and sign the result with Cosign v3 so nobody can swap it in your registry.
npm approve-scripts command reference
The official npm docs pages for these commands are thin, so here is the working reference in one place. Everything below is project-scoped, and none of it works with -g.
| Command | What it does |
|---|---|
npm approve-scripts --allow-scripts-pending | Read-only. Prints every package whose install scripts are not yet covered by allowScripts. No output means nothing is pending. |
npm approve-scripts <pkg> [<pkg>...] | Approves the named packages, writing version-pinned entries ("[email protected]": true) to allowScripts in package.json. |
npm approve-scripts --all | Approves everything currently pending in one shot. Read the pending list first. |
npm approve-scripts --no-allow-scripts-pin <pkg> | Approves name-only ("typescript": true), so future versions stay approved without re-review. |
npm deny-scripts <pkg> [<pkg>...] | Blocks the package's scripts with a name-only false entry. Denials are never pinned and approve-scripts will not overwrite them. |
Two behaviors that are easy to miss: ignore-scripts=true in .npmrc bypasses the whole allowScripts system silently, and the CI gate is just test -z "$(npm approve-scripts --allow-scripts-pending)", and it fails the build the moment anything is unreviewed.
Frequently asked questions
Is this a warning or an error?
A warning. npm 11.16 surfaces it but does not block the install. npm 12 turns it into a hard failure: packages with unreviewed install scripts will not run at all.
Will my CI break today?
No. Your install still runs on npm 11. It breaks when your runner upgrades to npm 12, estimated July 2026. The warning is your lead time, so use it.
Does it matter if it says "1 package" vs "2 packages"?
Only in that more pending packages means more packages to review before you are done. The fix is the same either way: npm approve-scripts --allow-scripts-pending lists them, then you approve or deny each one.
Does --ignore-scripts in .npmrc protect me?
No. If ignore-scripts=true is set in your project or CI, the allowScripts field is bypassed entirely and does nothing. You will think you are covered while the field has no effect. Drop ignore-scripts and use allowScripts as the single source of truth.
What happens to global installs and npx?
npm approve-scripts only works on project-scoped installs. For global tools and npx, you approve at install time with the install-level flags. This is a known gap tracked in npm/cli#9463.

Comments
Be the first to comment.