Skip to content
feed: live
>_ 0dayNews
supply chain
Analysis

npm 12 flips install scripts off by default

npm 12 lands with allowScripts, --allow-git, and --allow-remote all defaulting to none. GitHub is also winding down GATs that skip 2FA. The default just moved.

npm 12 flips install scripts off by default
Photo: Emcc83 / Wikimedia Commons · CC BY-SA 4.0
kilobaud Dave "Kilobaud" Ferris · Published · 4 min read

GitHub announced on Tuesday that npm 12 — now the latest tag on the registry — ships with a set of long-time defaults inverted. allowScripts is off, so the preinstall, install, and postinstall lifecycle hooks in a dependency will no longer run during npm install unless the local project has explicitly approved them. --allow-git and --allow-remote both default to none, meaning Git URLs and arbitrary remote URLs listed as dependencies are no longer resolved without an explicit opt-in. Implicit node-gyp builds — the mechanism a lot of native-module packages have historically used to compile against local toolchains during install — are disabled by default in the same release. The Hacker News has the release summary; the changelog entry is the primary source and worth reading before deciding what breaks.

The migration path GitHub proposes is npm approve-scripts --allow-scripts-pending, which produces an allowlist that a maintainer commits to package.json. The command is not a shrug; it is the point. You are meant to review the list, decide whose install-time code your CI trusts, and then commit that decision as project state.

There is a second piece to the announcement that has been slightly overshadowed by the install-scripts headline. GitHub is also winding down granular access tokens (GATs) that were configured to bypass two-factor authentication. Starting in early August 2026, those tokens lose the ability to perform sensitive account, package, and organization management — token lifecycle, password/email changes, 2FA configuration, package access changes, team management. Around January 2027, they lose the ability to publish directly; the terminal state is that a 2FA-bypass GAT is a read-only credential for private packages and a staging credential for publishes that a human still has to approve. Both dates are GitHub’s, from the same changelog post.

Analysis: this default has been the load-bearing wall for years

Analysis, not incident reporting. GitHub’s post does not name a specific compromise as the trigger for this change, and the changelog’s framing is forward-looking rather than a post-mortem. What follows is a read of the change, not a claim from GitHub about why they made it.

Install-time script execution — the ability for any transitive dependency to run arbitrary code on the developer’s machine, on the CI runner, and on whatever else touched npm install — has been the mechanism doing the actual damage in the majority of publicised npm supply-chain incidents for years. Not “one of the mechanisms.” Not “a complicating factor.” The mechanism. A malicious package published under a typo-squatted name, an account takeover of a legitimate maintainer, a lookalike inserted into the dependency tree of a widely-used package: the delivery vector is a compromised tarball on the registry, and the payload has almost always run via a postinstall script that fired the first time a downstream consumer typed npm install.

This is the same mistake, different decade. The default that just moved has been well-understood as dangerous for at least ten years of published incident write-ups. The counter-argument — that native modules need to build, that install scripts have legitimate uses, that flipping the default would break the ecosystem — has always been technically correct and has always been the reason nothing moved. What changed is not the technical picture. What changed is that the accumulated cost of leaving the door open, priced in maintainer time and compromised CI credentials and lost customer trust, finally exceeded the cost of breaking some builds.

The GAT deprecation is the smaller half of the same argument. A “granular” access token that can be configured to bypass 2FA on account management is not, in any meaningful sense, granular; it is a 2FA bypass with an audit-friendly name attached. Removing that in two phases over six months is a way of admitting the affordance existed and shouldn’t have, without a hard cut-off that strands legitimate CI workflows overnight.

What actually breaks, and what to do this week

For most projects with a well-maintained package.json, npm install under npm 12 will surface a set of packages that used to run install scripts and now don’t. Some of those scripts are load-bearing — a native module that needs to compile, a CLI that generates a config file. Others are not — telemetry, “install successful” postinstall banners, ad-hoc build helpers that have alternatives.

The honest workflow is not to disable the new default globally. It is to run npm install, note which packages surface as script-pending, and use npm approve-scripts --allow-scripts-pending to build an explicit allowlist that names them one by one and commits to package.json. That list becomes reviewable — a change to it shows up in a pull request the way any other dependency change does, which is the audit trail this default was designed to produce.

For projects publishing packages: if you have GATs configured to skip 2FA anywhere in your CI, treat August 2026 as the deadline for the account-management piece and January 2027 for the publish piece. The staged timeline means nothing has to be rewritten today, but nothing gets to be rewritten in a panic three days before either date either.

None of this makes an install-time compromise impossible. A package on the approved list can still ship a malicious update; a first-run installation still has to make a decision the first time. The default change reduces the blast radius from “every transitive dependency in the tree, every time” to “the packages you explicitly signed off on, when they update.” That is a meaningfully smaller number, and a meaningfully more auditable one, than what came before.

Sources

Found this useful? Share it.