Skip to content
Node.js

Node 26 LTS won't turn on npm 12's protections for you

Node 26 goes LTS on 28 October still bundling npm 11. How to install npm 12 yourself and switch to its install-script allowlist.

K7 min read
Node 26 goes LTS on 28 October still bundling npm 11. How to install npm 12 yourself and switch to its install-script allowlist.

On 28 October, Node 26 becomes Active LTS. Most teams will upgrade to it over the following weeks and reasonably assume they are on current defaults. They will not be: the installer still bundles npm 11, and npm 12 — the security release that stops dependency install scripts from running by default — will not arrive with any Node upgrade before Node 27 in 2027.

This article shows what npm 12 actually blocks, why the bundled npm will not bring it to you, and how to install it yourself and migrate to its allowlist without breaking your native modules or your CI.

What npm 12 stops running

Until npm 12, npm install ran the preinstall, install and postinstall scripts of every package in your tree, transitive dependencies included. Install meant execute: one compromised package anywhere in the tree ran code on every developer machine and CI runner that installed it.

npm's own announcement calls install-time lifecycle scripts the single largest code-execution surface in the ecosystem, and npm 12 closes it. Three npm install defaults change:

  1. Dependency install scripts are off. preinstall, install and postinstall from dependencies are skipped unless your package.json approves them.
  2. Git dependencies stop resolving. --allow-git defaults to none.
  3. Remote-URL dependencies stop resolving. --allow-remote defaults to none; --allow-file and --allow-directory keep their old defaults.
npm registrynpm installv12allowScriptspackage.jsonScript runsSkippedwarning onlytarballs with lifecycle scriptsevery dependency scriptapprovednot approved
Under npm 12 a dependency script only runs if your package.json approves it

The block also covers scripts nobody declared. Any package with a binding.gyp triggers an implicit node-gyp rebuild, and npm 12 blocks that too, along with prepare scripts from git, file and link dependencies.

The attacks these defaults answer

This is not a theoretical hardening. Every major npm supply-chain campaign of the past year executed through install-time scripts, before a single line of the package was ever imported.

Install-script campaigns kept growingCompromised npm packages per campaign, counts as reported by each vendor
Shai-Hulud · Sep 2025800ChainDrop · Aug 2026400Mastra · Jun 2026140@redhat-cloud-services · May 202632

Source: Group-IB, Microsoft Threat Intelligence, StepSecurity, Red Hat, 2025–2026

Install-script campaigns kept growing
Packages
Shai-Hulud · Sep 2025800
ChainDrop · Aug 2026400
Mastra · Jun 2026140
@redhat-cloud-services · May 202632

The ChainDrop worm ran from an npm preinstall hook before installation even completed, harvesting npm, GitHub, AWS and Vault credentials and republishing itself with the stolen tokens. Elastic's first remediation line for it was blunt: upgrade to npm 12, which blocks preinstall hooks by default.

The pattern has not slowed since. September's axios compromise smuggled a remote-access trojan in through a dependency's postinstall, and on 5 October StepSecurity found a credential-stealing payload in @subql/common.

Why your Node 26 upgrade doesn't bring it

npm majors normally reach you bundled with a Node release. Not this time: the Node.js Release Working Group decided not to land npm 12 on Node 26, 24 or 22 — it shipped too late for Node 26 and carries too many breaking changes for an LTS line.

So the dates fall badly:

DateWhat happensBundled npm
8 Jul 2026npm 12.0.0 ships as a security release—
20 Oct 2026Node 24 (Krypton) moves to Maintenance LTS11.x
28 Oct 2026Node 26 becomes Active LTS11.x
Apr 2027Node 27 is Current — the next chance for a bundled npm 12TBD
Oct 2027Node 27 becomes LTSTBD

The LTS dates come from NodeSource's October schedule summary; Node 27's from the new annual release schedule. On the day npm 12 shipped, Node 26.5.0 shipped alongside it still bundling npm 11.17.0.

Waiting for the bundled npm keeps the protections off for another year, through the ecosystem's worst run of install-script attacks.
InstallerNode 26 LTS28 Octnpm 11bundlednpm registrylatest is 12Your machine and CIinstallercomes alongnpm install -g npm@12
Upgrading Node does not upgrade the defaults; npm 12 is a separate install

Install npm 12 yourself

The good news: you do not need Node 27. npm 12's own engines line is ^22.22.2 || ^24.15.0 || >=26.0.0 (npm 12 changelog), so every currently supported Node line can run it. This is exactly how large projects such as Gutenberg are adopting it: decoupled from Node, installed explicitly.

Terminal
$ node -vv26.10.0$ npm -v11.17.0$ npm install -g npm@12$ npm -v12.0.2

Build your allowlist

After upgrading, npm install warns about every dependency script it skipped. The migration npm recommends is to snapshot what you already run, then tighten:

Terminal
$ npm installnpm warn install-scripts 3 packages had install scripts blocked$ npm approve-scripts --allow-scripts-pending$ npm approve-scripts --all$ git add package.json$ git commit -m "chore: snapshot install-script allowlist"

--allow-scripts-pending is read-only — it lists what needs a decision without changing anything. approve-scripts writes the allowlist into package.json and pins each approval to the installed version by default, so a hijacked later release is not pre-approved. From that commit on, you are protected against any new or changed script entering your tree.

Adding a new package with a script becomes a three-step loop instead of one command:

npm installnew packageScript skippedwarning printednpm approve-scriptsnpm rebuildScript has runnot yet approvedyou read what was skippedapproval written to package.jsonthe approved script executes
Adding a package with an install script is now install, approve, rebuild
Terminal
$ npm install -D esbuildnpm warn install-scripts esbuild postinstall skipped$ npm approve-scripts esbuild$ npm rebuild esbuild

For global installs and npx, approve-scripts refuses to run — there is no project package.json to write to. Use the config instead: npm install -g --allow-scripts=canvas,sharp <pkg>, or persist it with npm config set allow-scripts=canvas,sharp --location=user (the announcement's migration recipes).

The traps

An old ignore-scripts=true silently wins

Many teams already carry ignore-scripts=true in an .npmrc from an earlier hardening pass. While it is set, it takes precedence and no scripts run — the allowlist does not override it, and the same goes for a CI-wide npm_config_ignore_scripts environment variable. Build the allowlist first, then remove the blanket setting:

.npmrcDiff
- ignore-scripts=true

Strict mode and the package you haven't installed yet

strict-allow-scripts=true turns the skip into a hard error that fires before npm writes anything. A new package therefore never lands in node_modules, and approve-scripts then errors because there is nothing on disk to approve. The npm team's own guidance: keep strict mode in CI, not on dev machines — locally, the soft default plus approve-scripts plus rebuild is the workflow.

Native modules fail later, not at install

A blocked postinstall is not an install error. sharp, better-sqlite3, bcrypt and friends install fine with their build skipped, then fail at runtime when the compiled binding is missing — on npm 12 the failure moves from where you expect it to where you don't. Approve node-gyp and each native package explicitly, and grep the pending list for them first:

Terminal
$ npm approve-scripts --allow-scripts-pending | grep -Ei "gyp|sharp|sqlite|bcrypt"

allow-git is an enum, not a boolean

--allow-git and --allow-remote take none, root or all. A commenter in the migration thread reports that allow-git=true is treated as unset — a config that looks fixed and still fails. root only covers git dependencies declared in your root package.json, so a single transitive git dependency forces you to all; audit with a grep over your lockfile before npm 12 reaches CI.

Lock it in CI

The bundled npm on your CI image is npm 11, so the pipeline needs the same explicit install — and it is the right place for strict mode, because everything there is already approved and committed.

.github/workflows/ci.ymlYAML
jobs:  test:    runs-on: ubuntu-latest    steps:      - uses: actions/checkout@v4      - uses: actions/setup-node@v4        with:          node-version: 26      - run: npm install -g npm@12      - run: npm config set strict-allow-scripts true --location=project      - run: npm ci      - run: npm test

Now an unreviewed script fails the build loudly instead of being skipped quietly, and the only way to add one is a reviewed change to package.json. Treat that field accordingly: allowScripts is a code-execution permission list, so a pull request that expands it deserves the same scrutiny as a change to your deploy keys.

Before 28 October

  • Run npm -v on dev machines and CI; anything starting with 11 has the protections off.
  • Install npm 12 explicitly on one project: npm install -g npm@12.
  • Snapshot the allowlist: install, review --allow-scripts-pending, approve, commit.
  • Remove any ignore-scripts=true that would silently override the allowlist.
  • Approve node-gyp and every native module, then run the app, not just the install.
  • Grep lockfiles for git+ and remote tarball URLs before --allow-git=none hits CI.
  • Turn on strict-allow-scripts in CI only.

Upgrade to Node 26 by all means — it is the right move. Just carry npm 12 in with it, because this is the rare security default that will not come to you on its own.

Preparing for npm v12: install scripts and non-registry sources become opt-in · community · Discussion #198547Hi everyone — sharing this so maintainers, application developers, and CI operators have time to prepare for behavioral changes landing in npm v12 (estimated July 2026). Everything below is already...github.com
Found this useful? Share itXLinkedIn