· 9 min read

npm Finally Turned Off Install Scripts. Two Other Defaults in the Same Release Will Break Your Build First.

npm 12 makes three install behaviors opt-in that used to happen automatically. The headline is allowScripts defaulting to off, which means dependency preinstall, install and postinstall scripts no longer run unless you explicitly allow them. That change deserves the coverage it got. JFrog reports these vectors were involved in roughly 53% of the malicious npm attacks it observed over the past year, and npm is the last major package manager to close them; pnpm, yarn and bun have all blocked install scripts by default for a while.

But if you upgrade and something breaks, I would bet against the install-script change being the culprit. Two quieter defaults in the same release are more likely to bite first.

The two you have not read about

--allow-git now defaults to none. Git dependencies, direct or transitive, are not resolved unless you explicitly allow them. GitHub's stated reason is a code execution path that survived --ignore-scripts: a Git dependency's own .npmrc could override the Git executable. That is a genuinely nasty bug and blocking the source is the right call.

--allow-remote also defaults to none, which blocks dependencies pointing at https tarballs. --allow-file and --allow-directory are unchanged.

Now count how many projects you have with a line like "some-fork": "git+https://github.com/you/some-fork.git#main" in package.json. In my experience that is not rare in one-person projects; it is how you ship a fix to a dependency while your PR sits unreviewed for eight months. It is also how you consume an internal package before you have set up a private registry. Those installs stop resolving.

The failure is loud, which is good. But it is loud in CI at whatever hour your pipeline runs, and the error will point at a dependency you forgot was a Git URL.

The install-script change has a sharper edge than the summary suggests

Two details that the one-line summaries drop.

Implicit node-gyp builds are blocked too. Any package containing a binding.gyp is affected even when it declares no install script. If you have native modules, and you probably do somewhere in your tree, they will not build until allowed. prepare scripts from git, file and link dependencies get the same treatment.

A pre-existing ignore-scripts=true takes precedence and silently defeats the allowlist. Commenters on GitHub's migration discussion flagged this one, and it is the kind of interaction that produces a confusing hour. If you set ignore-scripts=true in an .npmrc years ago as a blanket precaution, approving specific packages will not do what you expect. Check your .npmrc files before you start approving anything.

There is also a chicken-and-egg problem worth knowing about: npm approve-scripts reads from node_modules, so it errors with ENOMATCH if the package is not installed yet. And global installs and npx cannot use approve-scripts at all; you configure them instead, for example npm config set allow-scripts=canvas,sharp --location=user.

The solo version of this migration is different

GitHub's migration guidance is to allow what is already in your tree first, then tighten. That is sensible, and it assumes something a one-person operation does not have: somebody other than you to review the allowlist.

In a company, deny-by-default plus an allowlist works because approval is a checkpoint where a second person looks. Alone, you are both the developer who wants the build green and the reviewer who is supposed to be suspicious. Those two roles are in direct conflict at 11pm.

Security researchers writing about npm 12 have made this exact criticism, and I think it is the strongest argument against the change as designed. One analyst at OpenSourceMalware pointed out that esbuild, sharp, core-js, puppeteer and bcrypt all rely on lifecycle scripts, and warned that repeated broken builds turn deny-by-default into a click-through prompt while pushing attacker activity onto surfaces with less visibility. That is not a hypothetical failure mode. It is how every permission dialog in computing history has ended up.

So the version I would actually run:

  1. Upgrade on a day you have slack, not before a release. The failures are loud and mostly quick to fix, but you want an hour, not five minutes.
  2. Audit .npmrc first. Every one of them, project and user level. Find and remove any blanket ignore-scripts=true, because it will silently override everything you do next.
  3. Allow what already exists, in one pass, and read the list while you do it. This is the only moment you will ever look at your full lifecycle-script surface with fresh eyes. Do not click through it. If a package you have never heard of runs a postinstall, that is the finding.
  4. Commit the allowlist to package.json and treat additions as real events. The allowlist in version control is the actual security benefit. A new entry showing up in a diff is a thing you can notice, which is more than you had before.
  5. Convert your Git dependencies to something else. A published private package, a vendored copy, or a patch via a patch tool. Each one is now a thing you must explicitly permit, and the honest question is whether you would permit it if you were seeing it for the first time.

Step five is the one I would push hardest. --allow-git defaulting to none is annoying, and the annoyance is doing you a favor: a Git URL in package.json is an unpinned, unaudited dependency on somebody's branch, and it was always the weakest line in your lockfile.

Where the criticism lands

The approval-fatigue argument is correct and the change is still worth shipping. Those are not in tension.

Deny-by-default does not stop a determined attacker; it moves the attack. If lifecycle scripts stop being a viable delivery mechanism, malicious code moves into the package's actual runtime, where it executes when your app imports it rather than when you install it. That surface has less tooling watching it. Anyone claiming npm 12 solves supply chain security is selling something.

What it does do is remove a category where the attacker gets execution on your machine merely because you typed npm install, without your code ever calling theirs. That distinction matters more for a solo operator than for a company, because your laptop is the production credential store. The npm token, the GitHub token, the AWS keys and the deploy SSH key are all in the same home directory. The keyv compromise earlier this month used a preinstall hook to steal exactly that set and then used the stolen tokens to spread. Removing install-time execution does not fix everything, but it means an unreviewed dependency has to at least wait for you to run it.

I will take that trade. Just do not upgrade the morning of a launch.

Author

Sources

Stay in the Loop

Get new posts delivered to your inbox. No spam, unsubscribe anytime.

Newsletter coming soon. Set PUBLIC_CONVERTKIT_FORM_ID in .env to activate.

Related Posts