npm v12 stopped running your postinstall scripts
published
TL;DR
npm v12 is generally available and ships three defaults that used to be the opposite. Dependency lifecycle scripts — preinstall, install, postinstall — and implicit node-gyp builds no longer run unless the package is on an allowlist. Git dependencies and remote-URL tarballs no longer resolve at all without a flag.
Snapshot what you already trust, commit it, then trim:
npm install
npm approve-scripts --all
git add package.json && git commit -m "chore: snapshot install-script allowlist"
The allowlist is a field in package.json, not a lockfile entry, so it reviews like code.
The problem
The failure is quiet. npm install exits 0, and the thing that used to appear after installing does not appear: no downloaded binary, no compiled native module, no generated file. The error only surfaces later, at runtime, from a package that assumed its postinstall had run.
Two shapes of this are common. A native module that compiles through node-gyp — the build simply does not happen. And a package that downloads a platform binary during install, which then fails when your code goes looking for it.
npm does tell you, at the end of the install: it lists the packages whose scripts were skipped. That list is the whole interface. If your CI collapses install output, you will never see it.
Why it happens
GitHub’s framing is that dependency lifecycle scripts are the single largest code-execution surface in the npm ecosystem — arbitrary code from every transitive dependency, executing on npm install, before anything has been reviewed. v12 closes that path by default, plus the two adjacent ones.
| Behavior | Before v12 | In v12 |
|---|---|---|
Dependency preinstall/install/postinstall | runs automatically | skipped unless in allowScripts |
Implicit node-gyp build | runs automatically | skipped unless allowed |
| Git dependencies (direct or transitive) | resolved | blocked; --allow-git defaults to none |
| Remote URL dependencies (https tarballs) | resolved | blocked; --allow-remote defaults to none |
Your own package’s scripts are not the target here — this is about code arriving from dependencies.
What to do
Start by looking, not approving. --allow-scripts-pending is read-only; it lists every package whose install scripts are not yet covered:
npm approve-scripts --allow-scripts-pending
Approve specific packages once you have decided each is one you actually want executing code:
npm approve-scripts sharp better-sqlite3
Entries are pinned to the installed version by default ([email protected]), which is the behavior you want: a version bump re-enters the review queue instead of inheriting approval. --allow-scripts-pin controls that if you disagree.
npm approve-scripts --all approves everything currently pending in one go. That is a migration tool, not a policy — it says “whatever I already had installed, keep working”. Run it, commit the diff, then delete the lines you cannot justify.
Git and remote-URL sources are governed separately from scripts, by --allow-git and --allow-remote. Both default to none, so a dependency pulled from a git URL or an https tarball — including a transitive one — stops resolving until you permit it explicitly. Auditing why one is there is usually a better use of the afternoon than allowing it.
If you are still on npm 11, you can rehearse the whole thing before upgrading. The features landed in 11.16.0, and --strict-allow-scripts turns on v12’s enforcement early:
npm install --strict-allow-scripts
That is the cheapest way to find out which CI job breaks, on your schedule instead of npm’s.
While you are in there: the 2FA-bypass token change
The same release note carries a second deadline that hits publishing rather than installing. Since early August 2026, npm granular access tokens configured to bypass 2FA can no longer perform sensitive account operations — creating or deleting tokens, changing password/email/2FA settings, changing package access or maintainers, or managing org and team membership. Those now require an interactive 2FA challenge.
Around January 2027, those tokens lose direct publishing entirely. Publishing moves to trusted publishing (OIDC) or staged publishing, where a human approves the release with 2FA. GitHub personal access tokens, GitHub App tokens and GITHUB_TOKEN in Actions are not affected by this change.
If a release pipeline currently holds a long-lived npm publish token, that pipeline has a rewrite due, and OIDC removes the token instead of rotating it.
Caveats
- This is npm only. pnpm and Yarn have their own settings for install scripts; nothing here changes them.
approve-scriptsneeds a project. It manages a field inpackage.jsonand does not work with--global.- Your CI image decides your npm version. A pinned older Node image keeps the old behavior until it moves — which is a reason to test with
--strict-allow-scriptsrather than wait to be surprised. - The allowlist is a review artifact. Committing it is the point: an added entry is a reviewable diff saying “this dependency may execute code on install”.
- Approving is still trust. An allowlisted package runs arbitrary code on every install, on every machine.
--allin a repo you have not read is the old default with extra steps.
References
- npm install-time security and GAT bypass2fa deprecation — GitHub Changelog, 8 July 2026
- npm-approve-scripts — npm CLI v12 docs
- Preparing for npm v12: install scripts and non-registry sources become opt-in — GitHub Community discussion
- Restricting npm bypass-2FA granular access tokens — GitHub Changelog, 31 July 2026