fix(config): avoid exporting persistent allow-scripts #9937

Merged
github-actions[bot] opened 1:49pm on August 31, 2026 wanted to merge 1 commit into npm/cli release/v11 from
backport/v11/9913

Pull Request Overview

  • Opened on August 31, 2026
  • Status Merged
  • Commit count 1 with first commit August 31, 2026

Total Delta

0 Total Diff Delta

Open Days

Open 15 weekdays

Test Delta

0 Diff Delta in Test Files
Breakdown by Phase

How long has this pull request spent in each phase of its lifecycle?

Fraction of total time Business days Phase
N/A 6.0 days Issue creation to work beginning
 
0.0 days Authoring 1 commit before pull request opened for review
 
1.8 days Awaiting first review
 
12.0 days Merge to deploy (still awaiting deployment)

Total time for pull request 1.8 business days from first commit authored to merge
Still awaiting deployment

Author avatar

fix(config): avoid exporting persistent allow-scripts

Backport of #9913 to release/v11.

Summary

Setting allow-scripts in a user or local (non-project) .npmrc makes a project's own package.json script fail with:

npm error code EALLOWSCRIPTS
npm error --allow-scripts is not allowed in project-scoped installs.
Add the entries to the "allowScripts" field in package.json, or to .npmrc, instead.

This happens even though the user never passed --allow-scripts β€” the value lives in a persistent .npmrc exactly as npm config set allow-scripts=<pkg> --location=user writes it β€” and even when the script explicitly passes --ignore-scripts.

The project package.json in question contains scripts like:

"scripts": {
  "inst": "npm ci --ignore-scripts",
  "perf": "node tests/ensure-playwright.mjs"
}
  • Running npm ci --ignore-scripts directly in the workspace: βœ… succeeds (no error).
  • Running npm run inst (which runs that exact same command): ❌ fails with EALLOWSCRIPTS.
  • Running node tests/ensure-playwright.mjs directly: βœ… succeeds.
  • Running npm run perf (which runs that exact same node … command, and that script internally spawns an npm install): ❌ fails with EALLOWSCRIPTS.

The only difference between the working and failing invocations is whether the npm install/npm ci process is reached through npm run-script. npm run exports the resolved npm config as npm_config_* environment variables for the lifecycle script's child process. Because the outer process read allow-scripts from a user/local .npmrc, the inner npm install/npm ci inherits npm_config_allow_scripts and reads it via its env source. resolveAllowScripts groups the cli and env sources together and rejects any policy from them in a project-scoped install, so the inner install aborts β€” even though the value originated in a .npmrc, not a command-line flag, and even though the inner install was told --ignore-scripts.

This is the same root cause as npm/cli#9783 (git-dep preparation), just reached through a different "shell-out to an inner npm install" path: an ambient npm_config_allow_scripts from a persistent .npmrc is misclassified as a forbidden CLI/env policy.

Environment

  • npm: 12.0.2
  • node: v26.6.0
  • OS: Windows 11
  • allow-scripts entry in user .npmrc: ["opencode-ai"]
  • node executable: C:\nvm4w\nodejs\node.exe
  • npm local prefix = C:\Users\xxx\xxx\xxx
  • HOME = C:\Users\xxx
  • cwd = C:\Users\xxx\xxx\xxx (a real project workspace, not global context)

How the offending setting gets there

This is the documented, recommended workflow. Installing a global package with an install script prints:

npm warn allow-scripts <pkg>@x.y.z (postinstall: node install.cjs)
npm warn allow-scripts Run `npm install -g --allow-scripts=<pkg>` to allow
  these scripts once, or `npm config set allow-scripts=<pkg> --location=user`
  to allow them for all global installs.

Following the second suggestion writes allow-scripts=<pkg> to the user .npmrc. From then on, npm run in any project on the machine that triggers an inner npm install fails β€” even ones that do not run any scripts and even ones that explicitly request --ignore-scripts.

Minimal, self-contained reproduction (no network / external repo required)

R=$(mktemp -d); cd "$R"
# A user/local .npmrc with the entry exactly as
# `npm config set allow-scripts=... --location=user` would write it.
echo "allow-scripts=whatever" > "$R/userrc"

mkdir app && cd app
npm init -y >/dev/null
# A script that spawns an inner `npm install` (mirrors `npm ci` / ensure-playwright.mjs).
mkdir inner && (cd inner && npm init -y >/dev/null)
cat > package.json <<'EOF'
{ "name": "app", "version": "1.0.0",
  "scripts": { "inst": "cd inner && npm install" } }
EOF

# (A) Direct inner install β€” works
(cd inner && npm install --userconfig "$R/userrc"); echo "direct exit: $?"

# (B) Same inner install, but reached through `npm run-script` β€” fails
npm run inst --userconfig "$R/userrc"; echo "npm-run exit: $?"

Actual result (B)

npm error code EALLOWSCRIPTS
npm error --allow-scripts is not allowed in project-scoped installs.
Add the entries to the "allowScripts" field in package.json, or to .npmrc, instead.

Expected result

Both (A) and (B) complete. An allow-scripts value that lives in a persistent user/local .npmrc is, by definition, not a command-line flag. When a script is run inside a project (notably a script defined in that project's own package.json), npm should ignore non-project (user/local) .npmrc allow-scripts rather than forwarding it into the child npm install as a forbidden env-layer policy β€” and it must never abort a command that was explicitly told --ignore-scripts.

Notes on the trigger

  • The failure requires the inner process to be an npm install/npm ci reached through npm run-script (or any lifecycle script that shells out to npm). The reproduction above uses a trivial inner install with no scripts at all, proving the policy check itself β€” not any real script execution β€” is what trips.
  • It is independent of where the setting lives: reproduced identically with the entry in the user .npmrc and the global (etc/npmrc) file. There is no persistent-config location that both silences the global-install script warning and leaves npm run working.
  • It reproduces even when the outer/script command passes --ignore-scripts, because the EALLOWSCRIPTS throw at resolveAllowScripts happens purely from the presence of the env-layer policy, before --ignore-scripts is ever consulted.

Root cause (source pointers)

  • lib/utils/resolve-allow-scripts.js:107 throws EALLOWSCRIPTS when a policy is found in the cli/env sources and the install is not global and not skipProjectConfig. The grouping:
  const cliPolicy = policyFromSources(npm, ['cli', 'env'])
  ...
  if (cliPolicy && !npm.global && !skipProjectConfig) {
    throw Object.assign(new Error(
      '--allow-scripts is not allowed in project-scoped installs. ' +
      'Add the entries to the "allowScripts" field in package.json, ' +
      'or to .npmrc, instead.'), { code: 'EALLOWSCRIPTS' })
  }
  • npm run-script exports the resolved config into the child process's environment as npm_config_* variables (so lifecycle scripts can read e.g. process.env.npm_config_registry). A allow-scripts value sourced from a user/local .npmrc therefore arrives in the inner npm install as an env-layer value, which policyFromSources(npm, ['cli', 'env']) reads and resolveAllowScripts treats as a forbidden CLI flag.

This is the same defect described in npm/cli#9783, where the ambient npm_config_allow_scripts instead arrives via the git-dep preparation inner install. The fix space is identical; npm run-script is simply an additional path that injects the env-layer value.

Suggested fixes

  1. (Preferred) When npm run-script (and the git-dep preparation path from #9783) spawns an inner npm install, do not forward npm_config_allow_scripts from the ambient environment into the child. The inner install already receives a deliberately curated flag set, so script policy for the inner build should be passed explicitly (or omitted) rather than inherited ambiently. This leaves the cli/env grouping in resolveAllowScripts intact per RFC 868, while fixing the ambient-env inheritance that misclassifies a .npmrc value as a CLI policy.
  2. Alternatively, in npm run-script, strip allow-scripts from the exported npm_config_* env when the script is project-scoped, so a user/local .npmrc policy is not re-applied to the project's own inner install.
  3. At minimum, fix the error message: when the value came from a persistent .npmrc (directly or via npm_config_* env inheritance), do not tell the user to "add the entries to .npmrc" β€” that is where it already is, and the advice sends them in a circle. The message should also not fire when the offending command was invoked with --ignore-scripts.

Related

  • npm/cli#9783 β€” same EALLOWSCRIPTS misclassification, reached via the git-dependency preparation inner install. This issue is the npm run-script sibling of that bug.

Workaround (for anyone hitting this)

Do not persist allow-scripts in a user/global .npmrc. Instead pass it only at the moment of the global install that needs it:

npm install -g <pkg> --allow-scripts=<pkg>

This approves the global install's script without leaving a persistent setting that later breaks npm run in unrelated projects. A project-scoped workaround is to move the policy into the project's own package.json allowScripts field (or project .npmrc) β€” but note that, per the linked #9783, the project-own-.npmrc path can still be hit via the env inheritance, so the robust fix is to avoid the ambient user/local entry entirely.

PR was closed without comments.