DEV Community

Indra Gusti Prasetya
Indra Gusti Prasetya

Posted on • Originally published at indragustiprasetya.com

Fix npm EALLOWSCRIPTS in project-scoped installs

You did the responsible thing. You turned on npm's new install-script allowlist so a freshly-published malicious version can't run postinstall code on your build box. Then you cloned a repo, ran npm install, and it died on a flag you never typed:

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.
Enter fullscreen mode Exit fullscreen mode

Read that last line twice. It tells you to move a setting to where you already put it. That is not a helpful error, that is a bug talking. Here is what actually broke and how to get a green build back without gutting the control you just added.

What the error really is

The feature is real and it's good. As of npm 11.17.0 and the npm 12.x line, install scripts are gated behind an explicit allowlist: an allowScripts field in package.json, managed with npm approve-scripts. If you shipped through the earlier rollout and hit the "N packages have install scripts not yet covered by allowScripts" warning, that's the same feature. I wrote up that warning and its fix in the npm allowScripts install-scripts guide, and it's worth reading first because this is a different failure with a different cause. That one was a warning nudging you to approve. This one is a hard EALLOWSCRIPTS stop.

EALLOWSCRIPTS almost never comes from your command line. It fires while npm prepares a git dependency: a package pulled from git+https://… or a GitHub shorthand that carries a prepare build step. Per npm/cli issue #9783, npm prepares that dependency by spawning an inner npm install. The outer config gets forwarded to that child as npm_config_* environment variables. The inner process then reads an allow-scripts value that came from your persistent user-level .npmrc, decides it's a forbidden project-scoped CLI policy, and kills the whole install. The value it's complaining about is an environment variable it inherited from itself. Nobody passed --allow-scripts on that command.

So the message is wrong in a specific way. It treats an env-inherited setting as if you typed it as a flag, then tells you to relocate it to a place it's already living. npm's own maintainers list "correct the error message to avoid contradictory guidance" as a minimum fix in #9783. When the tool admits the text is contradictory, stop trusting the text.

Why this bites teams that did the right thing

The trap is that the security guidance builds the failure for you. npm's docs and its warnings point you toward persisting the allowlist so it survives across projects:

npm config set allow-scripts=<pkg> --location=user
Enter fullscreen mode Exit fullscreen mode

Persist it that way, then clone any repo that pulls a package from git with a prepare step, and npm install dies with EALLOWSCRIPTS. You never scoped anything to that project. The config you set globally, on purpose, to be safer, is exactly what leaks into the git-dependency preparation and trips the check.

For a platform or DevSecOps team the shape of this is familiar. A pipeline goes red after a routine npm bump. The error blames a flag nobody in the repo passed. Someone greps the codebase for --allow-scripts, finds nothing, and burns an afternoon. Then the "obvious" fix surfaces: delete the .npmrc entry, watch the build go green, move on. That's the second-order bite. The hardening control does its job on the outer install and breaks the inner one, so the fastest path to green quietly removes a supply-chain defense you added deliberately. It's the same pattern I keep flagging: a control that works right up until it makes someone rip it out. The npm Trusted Publishing analysis is the cousin case, a defense that ships and still leaves the attack path open.

And the exposed population is growing, not shrinking, because the npm 12 install-script lockdown is reaching more teams every week.

The versions are current, not historical

This matters when you're deciding whether to wait it out. #9783 reproduces on npm 11.17.0, 12.0.1, and the main branch as of this writing. It is live. The reproduction is small: point at a git dependency that has a prepare script, set allow-scripts in a user-level .npmrc, then npm install a consumer of it. You don't need an exotic setup. A GitHub-shorthand dependency with a build step is enough.

The fix npm wants is upstream: strip npm_config_allow_scripts from the child environment during git-dependency preparation, and stop conflating env-inherited values with CLI flags. Until that release lands, the workaround is yours to run.

The sibling bug that makes approve-scripts untrustworthy

There's a related inconsistency worth knowing before you lean on npm approve-scripts as your source of truth. In npm/cli #9562 (npm 11.17.0, Node 24.16.0), npm ci --strict-allow-scripts rejected fsevents@2.3.3 with 1 package(s) have install scripts not covered by allowScripts. But npm approve-scripts --allow-scripts-pending never listed fsevents at all, because it's optional: true and os: ["darwin"]. The tool that builds your allowlist and the command that enforces it disagree.

Read what that does to a workflow. You run the approval tool on your Mac, it shows a clean list, you commit package.json, you feel done. Then CI on Linux (or a different OS matrix) rejects an optional dep the approval tool never surfaced. A "clean" approval run is not proof the enforcement pass will accept the same tree on another platform. For platform-specific optional deps, single-machine approval is a false positive.

What approve-scripts can and can't reach

One more boundary. Per the npm docs and issue #9463, npm approve-scripts maintains the allowScripts field inside a project's package.json. Run it against a global install and it errors with EGLOBAL. So for global CLIs and npx, you cannot pre-build a committed allowlist. You approve at install time with install-level flags instead. That's not a bug, it's a scope limit, but it catches people who assume one allowlist mechanism covers every install path. It doesn't. Project-scoped deps get allowScripts in package.json; global tools and npx get a flag on the install command.

If you're already treating fast-moving dependencies as a threat surface, this pairs with dependency cooldowns as a defense against fast supply-chain attacks: the allowlist controls what runs, the cooldown controls when a new version is even eligible to run.

How to get a green build back without losing the control

Work this in order. The first two steps fix the outage; the rest keep you from trading security for green.

  1. When EALLOWSCRIPTS hits a normal npm install, hunt for a git dependency first, not a stray flag. Open package.json and scan every dependency for git+, a raw GitHub URL, or an owner/repo shorthand. The one with a prepare script is where the inner install fires. Grepping your source for --allow-scripts is a dead end; the value is an inherited env var, not a flag in the repo.

  2. Stop persisting allow-scripts in a user or global .npmrc. That is the leak. Per #9783, remove the persistent setting so it can't be forwarded as npm_config_allow_scripts into git-dependency preparation, and pass --allow-scripts only on the specific install that genuinely needs it. Do not delete your allowlist to make the build pass. Those are different things: one is a persisted global env policy, the other is your durable per-project control.

  3. Keep the project-scoped allowScripts field as your real control. It lives in package.json, it's managed by npm approve-scripts, it survives this bug, and it shows up in a pull request where a reviewer can actually see what you allowed. Global .npmrc policy gives you none of that and triggers the failure. Prefer the committed field.

  4. Never trust a single-platform approval for optional deps like fsevents. Cross-check npm approve-scripts --allow-scripts-pending against what npm ci --strict-allow-scripts actually rejects on each target OS, per #9562. If your CI matrix spans macOS and Linux, run the enforcement pass on both before you call the allowlist complete.

  5. For global tools and npx, bake the flag into the install command. approve-scripts returns EGLOBAL there (#9463), so don't expect a persisted allowlist to cover them. Approve at install time.

  6. Pin your npm version and watch #9783. The genuine fix is upstream env-scrubbing. Until a release ships it, treat the workaround above as standing policy and re-test after every npm bump, because a version change is exactly what turns this back on. The same lockdown discipline pays off elsewhere: killing static credentials in CI, for instance, follows the same pattern in GitHub Actions OIDC to AWS.

The thing to hold onto: this error is the control working in the wrong place, not proof the control is wrong. Move the setting off the global env, keep it in package.json, and you get both a green build and the supply-chain gate you turned on in the first place.

Sources


Originally published at indragustiprasetya.com

Top comments (0)