BACK

Company Announcements

After 12 Years, npm Is Blocking Install Scripts by Default

Tom Franks

26 Jun 2026

Company Announcements

After 12 Years, npm Is Blocking Install Scripts by Default

Tom Franks

26 Jun 2026

Company Announcements

After 12 Years, npm Is Blocking Install Scripts by Default

Tom Franks

26 Jun 2026

No headings found in content selector: .toc-content

Back in 2014, a developer asked a simple question on Stack Overflow: how do I stop a package from running its postinstall script when I install it? The accepted answer was one flag, npm install --ignore-scripts, and the thread quietly racked up views for the next decade as more and more people went looking for the same thing.

Twelve years later, npm has decided to make that behaviour the default. With npm v12, preinstall, install, and postinstall scripts no longer run automatically when you install a dependency. After two years of supply chain worms, the industry's reaction has been a collective sigh of relief: about time. We want to make the opposite case. Turning install scripts off by default isn't npm fixing the problem. It's npm handing the problem to you.

What npm Actually Changed

For npm's entire history, installing a package meant running whatever code that package wanted to run. A dependency could declare a postinstall script in its package.json, and npm would execute it on your machine the moment it landed, no questions asked. That was a feature. It is also how most modern npm worms spread.

npm v12 flips the default. The headline changes are:

  • Lifecycle scripts are off by default. preinstall, install, and postinstall scripts in your dependencies no longer execute during npm install. You opt in, rather than opt out.

  • Approval is now an allowlist. Use npm approve-scripts to permit the packages that genuinely need to run code, and npm deny-scripts to block ones you have reviewed and rejected. The decision gets recorded so your team isn't asked again.

  • Native builds are gated too. A binding.gyp file triggers an implicit node-gyp rebuild, which is really just script execution under another name. npm v12 treats it the same way and holds it behind the same approval.

  • Git and remote dependencies need flags. Installing from a git URL now requires --allow-git, and fetching install-time payloads from arbitrary remote URLs requires --allow-remote. Both were quiet execution paths.

This sits alongside a wider set of registry changes shipping in the same window: seven-day token lifetimes, mandatory 2FA for publishing, and trusted publishing workflows. Together they go after how attackers get in and what they can do once a package is installed.

The change lands in July 2026, and the behaviour is already available in npm 11.16.0 and later behind warnings. If you want to see how much of your dependency tree will trip over it before npm makes the choice for you, you can preview which packages rely on install scripts:

npm approve-scripts --allow-scripts-pending
npm approve-scripts --allow-scripts-pending
npm approve-scripts --allow-scripts-pending

Hang On, You Could Always Do This

Here's the slightly awkward part. None of this is new capability. --ignore-scripts has existed for over a decade. You could already drop a single line into your .npmrc:

npm config set ignore-scripts true
npm config set ignore-scripts true
npm config set ignore-scripts true

And install scripts would stop running, globally, for everything. The flag was never a secret, the developers in that thread found it a decade ago.

The problem was never the switch. It was the default. Opt-out security protects the people who already know to ask the question, and almost nobody asked. Turning ignore-scripts on by hand broke legitimate native-module packages, produced confusing errors, and required a level of supply chain paranoia that most teams simply didn't have in 2014. So the flag sat there, used by a careful minority, while the default kept executing strangers' code on every machine in the build.

So npm flipped the default. That is the entire change: not a new capability, just a new default. And a default can't tell you whether the code you're about to run is safe. All it does is move that decision onto you, on every install, and trust that you'll make it well.

Why Install Scripts Were Such a Problem

Install-time execution is uniquely dangerous because of when it happens. As Aikido put it in their write-up, "the code executes the second you install, before you import anything or run your own code." You don't have to use the package. You don't even have to write a single line against it. The payload fires during npm install, which on most teams means it fires inside CI, right next to your cloud keys, your npm token, and your GitHub credentials.


A malicious install script igniting the moment a package lands in a CI pipeline, reaching for the credentials stored alongside it.

That timing is exactly why every significant npm worm of the past two years has leaned on it. ReversingLabs' analysis of the change makes the point plainly: it shuts down the execution path that Shai-Hulud, Mini Shai-Hulud, and Miasma all relied on to spread. The pattern is consistent across the incidents the industry has tracked:

  • Nx "s1ngularity" (August 2025) harvested roughly 2,300 credentials through a malicious telemetry.js postinstall script.

  • Shai-Hulud, the self-replicating worm that first tore through npm in September 2025 via the @ctrl/tinycolor compromise, used install scripts to seed itself into each new victim's projects and has come back in waves ever since.

  • The axios hijack (March 2026) planted a cross-platform RAT in a package pulling around 100 million downloads a week.

  • Mini Shai-Hulud (May 2026) spread with valid build provenance, so it sailed straight past the signature checks teams were relying on to catch it.

The cost of closing this path looks low at first glance. Only about 2% of packages on the registry define a postinstall script at all, so blocking by default removes a vector almost every worm depends on while seeming to touch only a sliver of the ecosystem. On paper, that makes it look like an easy win.

What This Doesn't Fix

Here's where we part company with the cheerful framing. Blocking install scripts isn't a free upgrade, and we wouldn't tell you to reach for it as your answer. Install scripts exist because real packages need to run code to install, so switching them off doesn't make that need disappear, it breaks the packages that depend on it and hands you a pile of approvals to sort out. Be honest about what that costs and what it leaves wide open, because the gaps are where the next wave of attacks already lives.

Preventing execution at install is very hard

The allowlist is a yes-or-no question: should this package be allowed to run code? It says nothing about whether that code is safe. You are approving a name and a version, not a behaviour, and a script that pulls its real payload from an attacker's server at install time looks identical, at approval time, to one that just compiles a binary. You approve the name. The name then goes and fetches the malware.

Switching off the obvious scripts doesn't even stop execution. When we pulled apart Miasma, a variant of the Shai-Hulud worm, in its "Phantom Gyp" attack on the LeoTech/RStreams packages, the trigger wasn't a postinstall script at all. It rode in a binding.gyp build-config file just a few lines long. When npm sees a binding.gyp and no prebuilt binary, it doesn't run a lifecycle script, it invokes the native build tool node-gyp on its own, in a separate process that --ignore-scripts never covered. The attacker used a normal build-syntax feature to run a shell command during the configure phase, before a single line was compiled, on every install. Turn the scripts off and the code still ran. v12 now gates binding.gyp too, so that particular path is closed, but the shape of it should worry you: npm shuts one execution path and the next campaign moves to another. A toggle is always one campaign behind.

Engineers can't review every package's install hooks

This is the part the announcement skips over. The average npm project carries 79 transitive dependencies, and plenty of the honest ones, like node-gyp, esbuild, sharp and puppeteer, have to build at install time. So here is what actually happens. You install a few hundred packages, nothing visibly breaks, and a wall of approval prompts is all that stands between you and a working build. Nobody reads them. You approve the lot, because the alternative is a package that won't compile, and npm ships an approve-everything flag for exactly that moment. The careful review the whole model leans on is the one thing a developer on a deadline will never do, so you either lose the functionality or you wave the malware through with everything else.


Approve-everything in practice: the queue gets rubber-stamped and the one malicious package rides through with the rest.

In CI and AI agents, there's no one to click "approve"

The whole approval model assumes a careful human sitting at a terminal. That is exactly who isn't there for the attacks that matter. Most of the worst install-time compromises of the past year went off inside CI runners, right next to the secrets that make a pipeline worth robbing in the first place. There's no human in a CI job to weigh a prompt: you either pre-approve an allowlist, and a freshly compromised dependency walks straight through it, or you approve everything to keep the build green. AI coding agents are worse again. They install packages on the fly to finish a task and will happily approve whatever they need to keep going. And persistence makes it stick: that same Miasma worm wrote hooks into .claude/, .cursor/ and .github/workflows/ files so it re-ran every time someone opened the project, nowhere near npm install. As Mini Shai-Hulud showed, even a signed, provenanced package can be hostile.

Where Ossprey Fits

npm v12 hands you a choice: approve a package's install script and run it blind, or block it and lose whatever it needed to do. Our take is that you shouldn't have to choose at all. You should get to have your cake and eat it, keeping the packages that genuinely need to build and dropping the ones that misbehave, without reading anyone's binding.gyp yourself.

That's the difference between asking "should this code be allowed to run?" and asking "what is this code actually trying to do?" Ossprey scores packages on their behaviour, on what the code attempts rather than whether it matches a known CVE. So when npm puts an install script in front of you, the call isn't a coin flip: you can see whether the script is doing what a build step should do, or reaching for your environment variables and phoning home.


Ossprey reads what a package actually does, so a hidden payload shows up before you ever decide to trust it.

That works in the places npm's switch can't reach: import-time code, CI and agent execution, persistence files, and packages that arrive with perfectly valid provenance. And because the read is based on what the code does, not on whether anyone has seen the package before, it works on brand-new packages too. A version published sixty seconds ago, that no feed has flagged and no analyst has opened, still gets assessed on its own merits. That is the window fast-moving worms live in. Ossprey runs alongside the tooling you already have, doesn't change how your engineers work, and only surfaces what's worth your attention, so an install-script decision becomes one you can actually stand behind.

Take Aways

  • Blocking install scripts is a blunt instrument, not a strategy. It breaks the packages that legitimately need to build at install time, and the approval prompt that's meant to make it safe is one you'll wave through to keep working.

  • Don't turn your engineers into the scanner. Let Ossprey decide which install scripts are safe to run, so you keep the packages that genuinely need to build and block the ones reaching for your secrets, without anyone reading a binding.gyp at 5pm on a Friday.

  • You can have it both ways. The choice between "packages that work" and "packages you trust" is a false one. Reading what code does gives you both; toggling a feature gives you one.

  • It's bigger than the install step. Import-time code, CI and agent execution, persistence files and forged provenance all sit outside npm's switch. Whatever you trust to make the call has to see those too.

Flipping this off by default isn't npm taking responsibility for the problem, it's npm handing the problem to you. npm sits at the centre of the ecosystem, and its answer to malicious packages is a prompt that asks every developer to make the call, blind, on every install. That's a lock on one door, not a guarantee about who you already let inside, and the attackers have already shown they'll just use another entrance. The real fix isn't a better switch, or a default, or an approval queue. It's knowing what a package actually does before you ever have to decide whether to trust it.

Ossprey can Help!

Ossprey scores open source packages on what they actually do, so you can approve install scripts with evidence instead of guesswork, and catch the malicious behaviour that slips past signatures and allowlists. It sits next to your existing tools without slowing your developers down, and only surfaces what's worth your time.

You don't have to take any of this on faith. Ossprey is in open beta, so you can point it at your own dependencies and see what turns up today: start with the open beta, or book a demo.

SHARE

Subscribe Now

Subscribe Now

Subscribe Now

Ossprey helps you understand what code is trying to do,  before you trust it.

Ossprey helps you understand what code is trying to do,  before you trust it.

Related articles.

Related articles.

Related articles.