By the end of this the repo fails npm ci the moment a dependency ships an install script nobody has reviewed, and the review decision sits in git where a colleague can argue with it. This is the step after committing an allowScripts block: not "which scripts run today" but "fail the build when that answer changes".
TL;DR: Putstrict-allow-scripts=truein a committed project.npmrc, runnpm ci, and an unreviewed install script becomesnpm error code ESTRICTALLOWSCRIPTSrather than a warning with exit 0. The catch is that three settings silence that gate:ignore-scripts=trueoverrides it,dangerously-allow-all-scriptsoverrides it, and packages you explicitly set tofalseare skipped without failing anything.npm config getis the only honest way to prove which of the three your runner is applying.
If you have not written the allowlist yet, start with the allowScripts schema in package.json and come back, because everything here reads that field. If you are still deciding which command writes it, approve-scripts vs --allow-scripts covers the two errors that reject each other.
Versions this depends on
- npm 12.2.1 or newer (
npm --version). npm 12.0.0 shipped 2026-07-08 with install scripts blocked by default; 12.2.1 and the v11 backport 11.21.1 both released 2026-10-15 and carry theinstall-strategy=linkedfix described near the end. - A project with a committed
package-lock.jsonand an existingallowScriptsobject inpackage.json. - Write access to the project-level
.npmrc(committed) and to the CI job definition. jqandgiton the runner for the drift checks.- Everything below assumes a single-package repo.
npm install-scriptsis documented as unaware of workspaces, so in a monorepo you run it once per package directory.
What does --strict-allow-scripts actually check?
Before changing anything, get the runner's real policy out of it:
npm config get ignore-scripts dangerously-allow-all-scripts strict-allow-scripts
npm config get takes multiple keys and prefixes each value with its key name, so one line covers the project .npmrc, the user .npmrc, and the environment at once. Read the first value first: any true there means the rest of this guide is inert until it is removed.
Then turn the warning into an error in a file, not in a CI argument:
# .npmrc (committed)
strict-allow-scripts=true
The npm v12 config docs describe the setting as taking the install-script policy "from a warning into a hard error: any dependency with install scripts that is not covered by allowScripts will fail the install instead of being blocked with a warning." Committing it to .npmrc keeps a developer's local npm ci and the pipeline on the same policy; a --strict-allow-scripts flag on one CI job gives you a gate that only exists where nobody is looking at it.
What strict mode evaluates is narrow: packages that carry install scripts and have no allowScripts entry. Approved packages run. Denied packages are skipped. Unreviewed packages fail the install. Three outcomes, and only one of them is red.
Why the gate can be on and the pipeline still green
Two documented overrides sit above allowScripts in precedence, and one of the three allowScripts outcomes is a silent skip by design. All three produce a green pipeline that looks like a pass.
--ignore-scripts overrides strict-allow-scripts per the config docs, and it also takes precedence over dangerously-allow-all-scripts. A pipeline hardened in 2024 or 2025 with ignore-scripts=true will never fail this check, and will never build a native module either. --dangerously-allow-all-scripts bypasses allowScripts entirely, including the false entries you wrote by hand; the docs frame it as a migration escape hatch and discourage its use. Set anywhere in the config chain, it means your deny list is documentation and nothing more.
The third path is the one that reads as nothing happening: denied packages do not fail strict mode. They have been reviewed, so they are skipped quietly and the install continues. Zero ESTRICTALLOWSCRIPTS failures with forty denials in package.json is the gate working exactly as specified, which is worth saying to the person who assumes a quiet build means an unprotected one.
Approve what you trust, then deny the rest by name
npm install-scripts ls
ls is read-only. It lists every package whose install scripts are not yet covered by allowScripts and touches nothing, per the npm install-scripts docs. The older spelling npm approve-scripts --allow-scripts-pending still does the same job; npm approve-scripts and npm deny-scripts are documented aliases of the approve and deny subcommands.
npm install-scripts approve esbuild sharp
Approvals default to pinned entries, so this writes "esbuild@0.28.2": true with the version you actually installed. Avoid approve --all here. It covers whatever happens to be pending at that moment, which includes anything that arrived in the same dependency bump you were trying to catch.
npm install-scripts deny --all
This answers the question the schema pages leave open: what to do with the long tail you will never audit one by one. Each becomes "pkg": false, the pending list empties, and the policy stays restrictive. Two documented properties make the deny pass worth doing first: denials survive a later blanket approve, and denied packages drop out of future review listings, so ls starts showing you only genuinely new arrivals. Denies are always written name-only, so one entry covers every future version of that package.
npm rebuild esbuild
An approval recorded after the install does not retroactively execute the script that was skipped. The tree stays structurally complete and functionally hollow, which is the same green-npm ci-then-missing-build/Release/*.node failure covered in npm ci exits 0 while build/Release fails on deploy.
Now prove the gate fires. Add a dependency with an install script that is not in allowScripts, then install:
npm install node-sass --package-lock-only
npm ci
echo "exit=$?"
Expect a non-zero exit and an error naming both the code and the uncovered package. The shape of the line, as pasted by the reporter of npm/cli issue #9562, is:
npm error --strict-allow-scripts: 1 package(s) have install scripts not covered by allowScripts: fsevents@2.3.3
And read the policy back out of the file rather than assuming it:
jq -r '(.allowScripts // {}) | to_entries[] | "\(.key)\t\(.value)"' package.json
jq '[(.allowScripts // {}) | to_entries[] | select(.value == false)] | length' package.json
The first prints one key<TAB>true|false line per entry, pinned keys included, so a wrong pin is visible. The second returns a single integer: the count of explicit denials, which is the number to quote when someone asks what the repo blocks.
The pin is the gate, so a version bump is a review
Bump esbuild@0.28.2 to 0.28.3 and the pinned key no longer matches the installed package, so the install fails until someone re-approves and the pin is rewritten. That failure is the design. --no-allow-scripts-pin converts the entry into a standing grant for every future version of that package, and it is the one decision here that should be made deliberately on a quiet afternoon, not reached for to clear a red build at 18:00.
Pins have a hygiene cost, which is what prune is for:
# .github/workflows/ci.yml (excerpt)
- run: npm ci
- run: npm install-scripts prune
- run: git diff --exit-code package.json
npm ci now carries the policy from .npmrc, so an unreviewed script fails the job before the build runs. prune removes allowScripts entries that no longer match an installed package, and git diff --exit-code fails the job when it changed anything, turning stale pins into a reviewable diff. The command documents a --dry-run flag, and I have deliberately left it off: a dry run that reports without writing leaves nothing for git diff to catch, so the job would go green while a human is expected to read log output nobody reads. Let prune write, and let the diff be the signal.
When strict mode fails on a package you cannot approve
Two defects produce a failure that imitates a policy violation, and both are worth checking your npm version against before you start arguing with a dependency.
npm/cli issue #9562, opened 2026-06-17, reported npm ci rejecting fsevents@2.3.3 on a platform where it is never installed, because the strict check walked the resolved tree while the approve command walked the installed one. PR #9597 (merged 2026-06-23) stopped flagging inert optional dependencies and was backported to v11 as #9602. On a current npm it should not reproduce; on an older v11 with strict mode on it will.
npm/cli issue #9939, opened 2026-09-01 against npm 11.19.0, is the nastier one. Under install-strategy=linked, approvals were written as keys like "file:../../../esbuild@0.28.2-5fSWMmpOnxTxpz8DaF55tQ/node_modules/esbuild": true, duplicated once per incoming symlink, so the key never matched the package and the same dependency failed strict mode no matter how many times it was approved. PR #9941 fixed the key derivation and shipped in npm 12.2.1 and 11.21.1 on 2026-10-15. On linked installs below those versions, upgrade first, then prune the store-path keys out. The subcommand family itself arrived via npm/cli PR #9424, which is recent enough that most generated answers stop at --allow-scripts-pending and never mention ls or prune.
The third failure mode is not a bug: npm install-scripts does not know about workspaces. Run it in each package that owns a package.json, and do not expect a root policy to cover the leaves.
FAQ
Does strict mode fail on packages I denied with false? No. A denied package has been reviewed, so its script is skipped and the install continues. Strict mode only fails on script-carrying packages with no entry at all.
Does ignore-scripts=true satisfy the gate? No, it disables it. The config docs state that --ignore-scripts overrides strict-allow-scripts, so nothing is evaluated and nothing fails. It also wins over dangerously-allow-all-scripts.
Why does the same package keep failing after I approve it? Either the pinned key no longer matches the installed version, or you are below npm 12.2.1 on a linked install and hitting issue #9939's store-path keys.
Does approving a script run it? No. Run npm rebuild <pkg> after the approval, or the skipped native build stays skipped.
What the repo carries afterwards
A committed .npmrc that makes the policy a build failure, an allowScripts block where every package with an install script is either pinned-approved or denied by name, a CI step that surfaces stale entries as a diff, and enough version detail to tell a real policy violation from the two bugs that imitate one. The next question is sequencing across more than one repo, since every machine still on npm 11.x disagrees with this config about the same commit: what breaks after CI goes green covers the order to roll it in and what each failure costs when it reaches production.
Comments
Be the first to comment.