By the end of this you will have a correct allowScripts block in package.json, a CI install that fails loudly when a dependency ships an unreviewed install script, and zero allow-scripts entries left in .npmrc where they cause a confusing error instead of a policy.
Short version: allowScripts is a plain object in package.json mapping a dependency to a boolean. true lets its install script run, false blocks it. Keys are either pinned ("[email protected]": true) or name-only ("pkg": false). It is a package.json field. .npmrc only holds the toggles around it (ignore-scripts, strict-allow-scripts, allow-scripts-pin) plus the allow-scripts list, which works for global and npx installs and is rejected in a project install.
The field shipped in npm 11.16 (commit a10c7ca, "Phase 1 of allowScripts opt-in install-script policy") and becomes enforcing in npm 12, which is late enough that most generated answers conflate the two config surfaces. npm's own error text conflates them too, so the confusion is earned. If you want the migration walkthrough rather than the semantics, read fix the "install scripts not covered" warning before npm 12; if you already hit the flag error, Fix npm --allow-scripts not allowed in project installs covers that one case. This page is the reference both of those point back to.
Prerequisites
- npm 11.16.0 or newer to see the pending-package warning, npm 12.x to see the default block. Check with
npm --version. - A project with a
package.json. The field is meaningless without one, andapprove-scripts/deny-scriptsreturnEGLOBALoutside a project. - Knowing which scripts are in scope:
preinstall,install,postinstall, andpreparefor non-registry (git or local) sources. Those are the only lifecycle scriptsallowScriptsgoverns. Your own root-project scripts always run.
Step-by-step
1. Find out which npm you actually have
npm --version
On 11.16 through 11.x a pending package produces a warning and nothing else. On 12.x the same package gets its script skipped by default. The behavior change is the reason a lockfile that installed fine last quarter can quietly stop running sharp's build step after a runner image bump.
2. List what is pending, without writing anything
# npm 12
npm install-scripts
# npm 11
npm approve-scripts --allow-scripts-pending
--allow-scripts-pending is read-only. It prints every package whose install scripts are not yet covered and touches nothing on disk, which is what you want on the first pass so you can review each package before approving it. Do the review here, not later: this list is the only point in the process where you look at what code you are agreeing to execute.
3. Write the field, and pick the key shape deliberately
allowScripts is Record<PackageIdentifier, boolean>. That is the entire schema.
{
"name": "my-app",
"allowScripts": {
"[email protected]": true,
"@swc/[email protected]": true,
"sharp": true,
"some-typosquat": false
}
}
Two key shapes exist, and choosing between them is the only real decision the field asks of you:
| Key shape | Example | Meaning |
|---|---|---|
| Pinned | "[email protected]": true | Approval is narrowed to that exact version. 0.24.3 is not covered and returns to pending. |
| Name-only | "sharp": true | Applies to any version of the package. |
Per the npm-install-scripts docs, npm writes pinned entries by default, because "By default it writes pinned entries ([email protected]), which keep their approval narrowed to the specific version you reviewed." The allow-scripts-pin config (default true) controls that; set it to false if you want generated entries to be name-only.
My rule: allow pinned, deny name-only. A true should expire when the code changes, and a false should not. Writing denials as "some-typosquat": false means the block survives every version bump, so you never re-review a package you already decided should never execute.
4. Rule out ignore-scripts before you debug anything else
npm config get ignore-scripts
If that returns true, allowScripts is inert. The docs are explicit that ignore-scripts "takes precedence and no scripts run, the allowlist does not override it." An .npmrc at any layer (project, user, global) can set it. Pick one model and stay in it: global-off via ignore-scripts, or granular via allowScripts. Running both means the allowlist is decoration.
Here is the resolution order for a dependency that declares an install script:
Three states matter for anything not blanket-disabled by ignore-scripts. Covered (true) runs normally. Denied (false) is blocked, and per the docs "Existing false entries always win; approve will not silently re-allow a package you previously denied," which holds even under approve-scripts --all. Pending (no entry) gets its script skipped with npm warn allow-scripts N packages have install scripts not yet covered by allowScripts, and the install still exits 0. The community migration discussion frames it the same way: "an install script you haven't approved gets skipped, you get a warning, and the install still succeeds."
That exit 0 is the part worth reacting to. A skipped postinstall on a native module does not fail the install; it fails at runtime, in whatever service imports the module, with a missing binary. The warning scrolls past in CI logs and the build goes green.
5. Turn pending into a hard failure in CI
# .npmrc
strict-allow-scripts=true
A pending package now fails the install with ESTRICTALLOWSCRIPTS instead of warning (npm/cli #9562). Set this in CI today, on npm 11, and you get npm 12's enforcement on your schedule rather than on your base image's. It pairs well with dependency cooldowns: a new version arrives, it lands as pending, and the pipeline stops until a human looks at it.
6. Keep the allowlist out of .npmrc
.npmrc can carry ignore-scripts, strict-allow-scripts, allow-scripts-pin, and allow-scripts=<comma,separated,names>. That last one only applies to global and npx contexts, where there is no package.json to hold a field:
# valid: global install, allow named packages' scripts
npm install -g --allow-scripts=canvas,sharp some-cli
# or persist it for global/npx use
npm config set allow-scripts=canvas,sharp --location=user
Pass --allow-scripts to a project install, ci, update, or rebuild and npm rejects it deliberately: it is "blocked in project installs on purpose (it's for -g and npx)."
The trap, filed as npm/cli #9783, is that a user or global .npmrc carrying allow-scripts=... can leak into a project install you never flagged. Preparing a git dependency spawns an inner npm install and forwards config as npm_config_* environment variables. The inner install reads allow-scripts off its env layer, classifies it as a command-line or env policy, and fails with EALLOWSCRIPTS. The message then tells you to move the entry into .npmrc, where it already is. Remove allow-scripts from your user and global .npmrc, and express project intent through package.json.allowScripts only.
7. Commit the field
node -p "require('./package.json').allowScripts"
git add package.json && git commit -m "chore: pin allowScripts allowlist"
allowScripts is a reviewed-code policy, so it belongs in version control with the same weight as the lockfile. Uncommitted, every teammate and every CI job re-hits the warning and someone eventually approves in a hurry.
Verify it works
npm ci --strict-allow-scripts
Expected on success: exit 0 with no allow-scripts warning, meaning every script-carrying dependency is covered. Expected on failure: a non-zero exit and ESTRICTALLOWSCRIPTS naming the offending package. Run it once with a deliberately bumped dependency version to confirm the pinned key stops covering it, since that is the behavior most likely to surprise you later.
Common pitfalls
- A version bump drops coverage.
"[email protected]": truedoes nothing for0.24.3. The package returns to pending, which is the design, not a bug. Reserve name-onlytruekeys for packages you trust across every future version. ignore-scripts=truemasks a working allowlist. Scripts not running despite a completeallowScripts? Checknpm config get ignore-scriptsfirst, including the user and global layers.--allow-scriptsis not the granular tool. It is for-gandnpx. Copying it out of a global-install tutorial into a project command producesEALLOWSCRIPTS, and #9783 means you can hit that error without typing the flag at all.approve-scriptsanddeny-scriptsfail outside a project withEGLOBAL, because there is nopackage.jsonto write. Use theallow-scriptsconfig for global andnpxwork.- A green install is not a working install. On npm 12 a pending native module installs cleanly and breaks at import time. If you are already chasing odd install-tree behavior, rule out lockfile damage separately, as in Fix npm "Cannot read properties of null (edgesOut)".
FAQ
Does allowScripts go in package.json or .npmrc? The per-package allowlist lives only in package.json. .npmrc carries the toggles (ignore-scripts, strict-allow-scripts, allow-scripts-pin) plus the allow-scripts list for global and npx installs.
What is the difference between "pkg": true and "[email protected]": true? Name-only allows any version. Pinned allows only the version you reviewed, and a new version resets to pending. npm writes pinned entries by default.
Does allowScripts override ignore-scripts? No. With ignore-scripts=true, no scripts run and allowScripts is ignored entirely.
What happens to a package that is not listed? It is pending: skipped with a warning on npm 12, install still exits 0. With strict-allow-scripts set, the install fails with ESTRICTALLOWSCRIPTS.
Do false entries ever get re-enabled automatically? No. An existing false wins, and approve-scripts --all will not undo it.
Wrap-up
The model in one line: allowScripts is Record<PackageIdentifier, boolean> in package.json, pinned by default, false always wins, and ignore-scripts short-circuits all of it. The per-package allowlist never belongs in .npmrc; that file holds the toggles and the global-only allow-scripts list.
Do these in order. Run npm install-scripts (or approve-scripts --allow-scripts-pending on 11.x) and read the list before approving anything. Write approvals pinned and denials name-only. Set strict-allow-scripts=true in CI so an unreviewed script fails the build now instead of after your runner picks up npm 12. Grep your user and global .npmrc for allow-scripts and delete it. Commit package.json. The reason to bother is the same reason build.rs in Cargo deserves an allowlist it does not have: an install script executes with your developer's credentials and your runner's network access, before any code you wrote has run.
Sources
- npm-install-scripts | npm Docs (v12)
- Preparing for npm v12: install scripts become opt-in, community Discussion #198547
- npm/cli #9783, user/global .npmrc allow-scripts leaks into git-dependency prep
- npm/cli commit a10c7ca, Phase 1 of allowScripts opt-in policy
- npm/cli #9562, strict-allow-scripts behavior
Comments
Be the first to comment.