LabRoundupColumnNews
blog/Articles/npm v12: Dependency Install Scripts No Longer Run Automatically
npm-v12-install-scripts-opt-in-no-auto-run-cover-en

npm v12: Dependency Install Scripts No Longer Run Automatically

With npm v12 (July 2026), npm install stops auto-running dependency install scripts. We reproduce a postinstall running on its own, explain what changes, the benefits you never noticed, and how to prepare today.

Lab Updated today
avatar-m-1

Makoto Horikawa

Backend Engineer / AWS / Django

2026.06.119 min0 views
Key takeaways

With npm v12 (July 2026), npm install stops auto-running dependency install scripts. We reproduce a postinstall running on its own, explain what changes, the benefits you never noticed, and how to prepare today.

The bottom line: npm v12 stops "npm install" from silently running scripts

In npm v12, slated for July 2026, the behavior of the command you type every day, npm install, changes. Until now, the "install-time scripts" shipped by the dependencies you install were run automatically, without your permission. From v12, that is blocked by default, and only the scripts you explicitly allow will run.

For anyone who writes JavaScript or TypeScript, npm install is probably the single most-typed command. When the default behavior of that command changes, the blast radius is wide. And the reason for the change is the supply-chain attacks that have plagued npm for years.

"A dependency's code runs with my privileges just because I installed it" can be hard to picture. So before anything else, I ran it on my own machine to see it happen. Look at the terminal output below, which stands in for a screenshot.

A single npm install let code planted in a dependency write a file into my home directory. The screen only said "added 1 package." By default there is not even a trace shown that anything ran — and that is the behavior v12 removes.

What was npm install actually doing all this time?

When you install a package with npm (Node.js's standard package manager), npm doesn't just download and unpack the dependency. If a package's package.json declares lifecycle scripts, npm runs them at set moments. The three most common are:

  • preinstall: runs right before installation
  • install: runs during installation (used for building native modules, etc.)
  • postinstall: runs right after installation (the most commonly used one)

The problem is that these run automatically not only for your own project, but for your dependencies (and their dependencies, and so on). Code from a package buried deep in node_modules — one whose name you've never heard — runs with your user privileges the moment you type the command.

And it isn't only scripts that were resolved automatically before v11. Here is what npm officially lists as changing in v12.

Done automatically before v11What it means
Install-time scriptsAuto-runs a dependency's preinstall / install / postinstall
Native buildsAuto-runs node-gyp rebuild for packages with a binding.gyp
prepare scriptsAuto-runs prepare for git / file / link dependencies
Git dependenciesAuto-resolves references to repositories such as GitHub
Remote-URL dependenciesAuto-resolves URL references such as HTTPS

In v12, everything in this table becomes "won't run unless explicitly allowed."

I tried it: a dependency's postinstall runs on its own

Words alone don't land, so I reproduced it with the npm on my machine (npm 10.9.7 / Node.js 22 at the time of writing; the auto-run behavior is identical through v11). What I did is simple: I wrote a single malicious-dependency stand-in and added it to an ordinary project.

The dependency's package.json is just this. Its postinstall writes a file into the home directory (a real attack would steal credentials or exfiltrate data here).

{
  "name": "evil-dep",
  "version": "1.0.0",
  "scripts": {
    "preinstall":  "echo 'dependency code is now running with my privileges'",
    "postinstall": "node -e \"fs.writeFileSync(os.homedir()+'/.npm-demo-pwned', 'stolen')\""
  }
}

Now run npm install in a project that depends on it. What's scary is that by default, nothing is shown.

$ npm install
added 1 package in 300ms

$ ls -a ~ | grep npm-demo
.npm-demo-pwned          # <- and anyway< file pre ran script the wrote>

      

It only said "added 1 package," yet .npm-demo-pwned now exists in the home directory. The script definitely ran, but that fact never appears on screen. Only with --foreground-scripts can you finally see what happened inside.

$ npm install --foreground-scripts

> evil-dep@1.0.0 preinstall
> echo 'dependency code is now running with my privileges'
dependency code is now running with my privileges

> evil-dep@1.0.0 postinstall
> node -e "...write a file into the home directory..."
wrote a file into the home directory (= arbitrary code execution succeeded)

added 1 package in 98ms

This is the pre-v12 default. The behavior closest to v12's new default can be felt on today's npm with --ignore-scripts (strictly speaking, --ignore-scripts also stops your own project's scripts, whereas v12 only stops dependencies' scripts — a more surgical change).

$ npm install --ignore-scripts
added 1 package in 87ms

$ ls -a ~ | grep npm-demo
                         # <- blocked< created. nothing pre script the was>

      

Just to be sure, I checked the default value. npm config get ignore-scripts returns false. In other words, "don't ignore scripts = run them automatically" is the current default. It helps to think of v12 as effectively flipping that default.

Why change it: this was the main route for supply-chain attacks

Why is npm going this far? Because the postinstall I reproduced above is the classic entry point for supply-chain attacks targeting npm. Attackers hijack a popular package, or publish a typosquatted fake, and plant malicious code in its postinstall. Then they just wait for someone to npm install. The victim does nothing, yet the code runs.

This isn't theoretical. We've covered real incidents on this blog many times.

"Then just always pass --ignore-scripts," you might think. npm acknowledges that, and the v12 RFC explains that a .npmrc bundled in a git dependency can override the path to the git executable, providing a route to code execution even when you use --ignore-scripts. In other words, the old self-defense left a hole. So the decision is to tilt the default itself to the safe side.

If you want to audit your dependencies for vulnerabilities yourself, see our OSS supply-chain scanner (paste your npm / Python dependencies for an instant check).

What exactly changes in npm v12

The heart of v12 is not a ban but an opt-in. Rather than making scripts unusable, it switches to a model where you name the ones you want to run and allow them. The allowlist is recorded in a new allowScripts field in package.json, and committing it to git to share across the team is recommended.

You manage this with two new commands added to npm.

What you want to do Command / flag
List dependencies that would be blocked npm approve-scripts --allow-scripts-pending
Allow a dependency you trust npm approve-scripts
Explicitly deny a dependency npm deny-scripts
Allow git dependencies --allow-git
Allow remote-URL dependencies --allow-remote

Another key point: approvals are pinned per version. For example, approving esbuild@0.21.5 does not silently extend to esbuild@0.22.0. Every time a version bumps, a human re-checks it. Hijacks tend to happen when "a package you trusted ships malicious code in some later update," so this version pinning makes sense.

The release is slated for July 2026. It won't arrive out of nowhere: npm 11.16.0 and later already show warnings up front, so you can prepare for the migration.

Auto-execution also brought benefits you never noticed

I've written "auto-execution is dangerous" up to here, so in fairness, here's the other side. Auto-running postinstall has long quietly powered conveniences engineers enjoyed without ever thinking about them. In fact, the reason "everything just works after npm install" is precisely this mechanism.

What auto-execution powered Examples
Building native modules better-sqlite3 / bcrypt compiling C/C++ for your environment
Downloading binaries and browsers esbuild / Cypress / Puppeteer fetching executables and browsers
Transpiler post-processing @swc/core / core-js initializing via postinstall

So what happens to the specific libraries you use? I checked each package's install-time scripts directly against the npm registry and summarized the impact below (verified against the latest versions at the time of writing).

Popular library What it does at install time What breaks if you don't allow it
esbuild postinstall fetches a platform-specific binary esbuild itself won't run; many build tools such as Vite depend on it internally, so the impact is wide
better-sqlite3 install runs a node-gyp native build the SQLite driver fails to load and DB access breaks
bcrypt install runs a native build password hashing won't work
Cypress postinstall fetches the test-runner binary E2E tests won't launch
Puppeteer postinstall downloads Chromium itself no browser to drive; scraping and testing become impossible
@swc/core postinstall does native-related post-processing builds and transpilation may be affected
core-js postinstall runs post-processing (a message, etc.) impact is minor; usually fine even if left unapproved

Conversely, the widely used Husky (a git-hooks manager) is on the less-affected side. Husky's hook setup runs not from a dependency but from the prepare script you write in your own package.json. v12 only blocks dependencies' scripts, so scripts you wrote in your own project keep running as before. Likewise, libraries such as sharp that recently dropped install-time scripts in favor of shipping prebuilt binaries are unaffected.

These ran silently behind npm install, and we got a "working environment" without even being aware they existed. From v12 on, even these legitimate, useful scripts need your approval the first time. In exchange for safety, a tiny bit of effort comes back. The shift is from "silently do everything" to "ask once whether it's OK" — and the only one inconvenienced by being asked is the attacker.

Try it on your own package.json: an impact checker

A cheat sheet alone doesn't answer "so what about my project," so here's a small checker. Paste your entire package.json and press "Check," and it surfaces the packages in your dependencies (and the like) that are likely to need approval in v12.

Everything runs entirely in your browser (nothing is sent anywhere). The verdict is a match against a list of known packages I actually verified against the npm registry.

Note: this checks only direct dependencies (those listed in package.json) against a known list. Deeper transitive dependencies, and minor or in-house packages not on the list, can't be judged here. For an accurate list, run npm approve-scripts --allow-scripts-pending on npm 11.16.0 or later.

What to do now, before v12 arrives

Rather than waiting for July, it's safer to get used to it now. The steps are simple.

  • 1. Move npm to 11.16.0 or later (npm install -g npm@latest)
  • 2. Run npm approve-scripts --allow-scripts-pending to list dependencies that ship scripts
  • 3. Review them and approve only the ones you trust with npm approve-scripts
  • 4. Commit and share the updated package.json

Pay special attention to CI (continuous integration). Projects that run npm ci in a clean environment may find that if you forget to commit the allowlist, native builds and binary downloads stop the moment you move to v12, breaking the build. Get allowScripts in order ahead of time and that day passes quietly.

Wrap-up: from "silently do it" to "ask just once"

After running it myself, the scariest part wasn't the attack itself but the silence: it only said "added 1 package," yet code had run. I had to admit I'd barely thought about what that command, which I type constantly, was doing behind the scenes.

The v12 change trades away a little convenience to erase that silence. The benefits we used to get automatically now have to be acknowledged once, as an "approval." It's more effort, but the only one inconvenienced by that effort is the attacker. For one of the most-typed commands in the world, npm install, it feels like a reasonable landing spot. Before July arrives, give approve-scripts a try.

References