GitHub Disables npm's Auto-Run Install Scripts, Closing the Door Shai-Hulud Walked Through
#Security

GitHub Disables npm's Auto-Run Install Scripts, Closing the Door Shai-Hulud Walked Through

Privacy Reporter
6 min read

Starting with npm 12 in July, the install command will stop executing dependency scripts automatically. It is the same feature the Shai-Hulud worm abused to spread, and the change finally brings npm in line with safer defaults its competitors adopted years ago.

GitHub is about to remove one of the most dependable footholds attackers have had inside the JavaScript ecosystem. With the release of npm 12 in July 2026, the npm install command will no longer run lifecycle scripts automatically. For developers who have watched supply chain attacks march through the npm registry for years, the only surprising thing is that it took this long.

Featured image

What happened

Every time you run npm install, npm has historically executed any preinstall, install, or postinstall scripts defined by the packages you pull in. Crucially, that includes not just the package you asked for, but every transitive dependency buried in your tree. A modern web project can easily resolve to hundreds or thousands of packages, and until now, each one was granted the right to run arbitrary code on your machine the moment it was installed.

Leo Balter, the maintainer behind the change, did not mince words. "Install-time lifecycle scripts are the single largest code-execution surface in the npm ecosystem," he said. "Every npm install runs scripts from every transitive dependency, so a single compromised package anywhere in your tree can execute arbitrary code on a developer machine or CI runner."

That is precisely the mechanism the Shai-Hulud worm exploited. By compromising packages and lacing their install scripts with self-propagating malware, the worm turned the routine act of installing dependencies into an infection vector that spread across developer laptops and automated build pipelines alike.

Three defaults are changing in npm 12:

  • Lifecycle scripts are off by default. preinstall, install, and postinstall scripts will not run unless explicitly permitted through an allowlist.
  • --allow-git defaults to off. This flag pulls dependencies directly from remote Git URLs. Disabling it closes an attack path where a malicious .npmrc file could override the Git executable and achieve code execution.
  • allow-remote defaults to none. Dependency downloads from arbitrary remote URLs are blocked entirely.

Approval for scripts lives in an allowlist inside package.json, and by default that approval is pinned to the specific installed version of a package. If a future version of that package suddenly ships a new install script, it will be blocked until a human signs off again.

Why it matters for the people running the code

The distinction that matters here is who bears the risk. When a developer types npm install, they are making a decision about their own project. They are not knowingly agreeing to execute code written by the maintainer of some six-levels-deep utility they have never heard of. The old default quietly transferred trust from the developer to the entire dependency graph, without consent and without visibility.

This is the same pattern that shows up across data protection law: systems that collect or execute by default, placing the burden on the individual to opt out of something they never agreed to in the first place. npm's new posture flips that. Code execution now requires an affirmative, version-pinned decision. It is consent by design rather than exposure by default.

The practical fallout reaches well beyond individual machines. Continuous integration runners, the automated servers that build and test code, were among the juiciest targets precisely because they install dependencies constantly and often hold credentials. A compromised postinstall script running on a CI runner can exfiltrate secrets, publish poisoned packages, and reach further into an organization's infrastructure. Shutting off automatic execution narrows that blast radius considerably.

What developers need to do

These are breaking changes, and Balter was upfront about the migration pain. The recommended first move is to run the commands that allow scripts for every package in an existing project that currently relies on them. "This gets you protected against new, unexpected scripts immediately," he said. The second step is to go back through that list and deny scripts for any package that does not genuinely need them.

Some packages legitimately depend on install scripts to function. Native modules that compile on installation need them. Testing tools like Playwright and Puppeteer fetch browser binaries through postinstall. Electron, which bundles the Chromium engine to build cross-platform desktop apps, does the same. These will require explicit approval to keep working.

The building blocks have actually been available since npm 11.10.0, released in February 2026, but as opt-in flags rather than defaults. That release also introduced min-release-age, which refuses to install package versions newer than a configurable number of days. It is a deliberate quarantine against freshly published malicious releases, which tend to be caught and pulled within hours or days. Developers on the current npm 11.16 can set these flags now in .npmrc or through environment variables, both hardening their projects today and smoothing the eventual jump to npm 12.

There are sharp edges. The older ignore-scripts setting does not support an allowlist on its own, and it overrides the new allow-scripts behavior. Anyone who set ignore-scripts to true in the past will need to remove it before approved scripts can run. The allowScripts setting that exists in npm 11 is advisory only for now.

What changes, and what does not

Nobody involved is calling this a cure. "Now all the malware can move from the install script to the module itself where it will inevitably still be run," one developer noted, and the point is fair. A compromised package whose code you actually import and execute remains dangerous regardless of install-time policy. Disabling auto-run scripts raises the cost and reduces the convenience of one popular attack technique. It does not eliminate the underlying problem that you are running other people's code.

The change also sharpens an ongoing argument about alternatives. pnpm has blocked install scripts by default for several versions and ships its own minimum release age safeguard. The pull request for the npm change states the situation plainly: "npm is the only remaining major package manager that runs dependency install scripts by default. pnpm v10+, Yarn Berry, Bun, and Deno all block them." npm, the largest and most widely used of the bunch, was the last holdout.

For the broader supply chain, the significance is less about any single flag and more about the shift in default trust. Defaults are policy. They decide what happens to the overwhelming majority of users who never change a setting, and for years npm's defaults handed silent execution rights to anyone who could get a package into a dependency tree. That arrangement is ending. It arrives late, after worms like Shai-Hulud demonstrated the cost in concrete terms, but it moves the ecosystem toward a model where executing untrusted code is a choice someone makes rather than a side effect of installing software.

Comments

Loading comments...