Web Architecture

How Many npm Packages Actually Run Install Scripts?

npm 12 blocks dependency install scripts by default. I measured 11 dependency trees, 3,602 packages in total, to find how much that actually breaks.

September 6, 20269 min read
npmdependenciessupply chainengineeringmeasurement

The short answer: far fewer than the coverage implies. Across eleven resolved dependency trees totalling 3,602 packages, only 22 declared an install script — 0.61% of everything installed. The median tree ran two. The largest tree measured, at 997 packages, ran five. One tree ran none at all. Eight distinct packages accounted for every install script in the sample, and nine of the 22 were fsevents, a macOS-only package that never installs on a Linux CI runner. Migrating a real project to npm 12 is usually a two-to-five line allowScripts field, and you can compute yours before you upgrade.

npm 12 stops running dependency install scripts unless you approve them. Every article about that change explains the mechanism, and none of them tells you how many packages in your project are affected. That number is knowable in advance, from a file you already have committed, so I measured it.

What counts as an install script?

An install script is a lifecycle hook a package declares in its own package.jsonpreinstall, install, or postinstall — that your package manager executes automatically when the package is installed. It runs with your user account, your environment variables, and your network access, on your laptop and in CI. That is why it became the favourite delivery mechanism for npm malware: JFrog's security research puts lifecycle scripts behind roughly 46% of malicious npm packages over the past year.

npm records this for you. In package-lock.json at lockfileVersion 2 or 3, every package carries a hasInstallScript flag, which npm's own documentation defines as "a flag to indicate that the package has a preinstall, install, or postinstall script". Counting those flags measures a full transitive dependency tree without installing anything.

How many packages in a real application run one?

I resolved eleven dependency trees on 4 September 2026 and counted. Ten are dependency manifests written for this study, covering stacks a small team actually ships; the eleventh is this site's own production lockfile. Every dependency was requested at the latest tag, so this is a dated snapshot rather than a claim about all time.

Dependency treePackages in lockfileRun install scriptsShare
Next.js + Playwright + Vitest + Storybook99750.50%
jagatjeet.com (real production Next.js app)47120.42%
create-next-app (TypeScript + Tailwind + ESLint)44410.23%
Astro + Tailwind34920.57%
Gulp + Sass front-end build30120.66%
Next.js + Prisma + Auth.js24020.83%
React Router v721110.47%
Express + Postgres API20920.96%
Next.js + tRPC + Drizzle17542.29%
Next.js + Supabase + Stripe13700.00%
Vite + React + TypeScript6811.47%
All eleven trees3,602220.61%

The count per tree runs from zero to five, with a median of two. Notice what does not happen: the number does not scale with the size of the tree. The 997-package tree runs five install scripts; the 175-package tree runs four. Adding dependencies adds packages you have to trust, but it barely adds packages that execute code at install time.

One caveat I would rather state than bury. The denominator is lockfile entries, which include platform-specific optional packages for every operating system, not only the one you build on. So does the numerator — which is exactly why the next section separates them out.

Which packages are they?

Eight distinct packages account for all 22 install scripts in the sample. Every script below was read from the package's own manifest on the npm registry.

PackageScript it declaresWhat it doesTimes in sample
fseventsinstall: node-gyp rebuildNative macOS file-watching binding. Its manifest declares "os": ["darwin"], so npm never installs it on Linux or Windows.9
esbuildpostinstall: node install.jsLinks the platform-specific bundler binary.6
unrs-resolverpostinstall: node postinstall.jsNative module resolver, pulled in by the ESLint config.2
sharpinstall: node install/check.js || npm run buildNative image processing, pulled in by Next.js for image optimisation.1
prismapreinstall: node scripts/preinstall-entry.jsPrisma CLI entry check.1
@prisma/enginespostinstall: node scripts/postinstall.jsDownloads the query-engine binaries.1
@parcel/watcherinstall: node scripts/build-from-source.jsNative file watcher.1
core-js-purepostinstall: node -e "try{require('./postinstall')}catch(e){}"Prints a funding message. Nothing else.1

Three things fall out of that table.

fsevents is nine of the 22, and it is macOS-only. On a Linux CI runner those nine never execute, because npm skips packages whose declared os does not match the platform. Excluding it, the sample drops to 13 install scripts across 3,602 packages — 0.36%.

Six of the remaining thirteen are esbuild, which showed up at three different versions inside one project. That is not three risks; it is one decision, counted three times.

One of the twenty-two does nothing for your build. core-js-pure uses its postinstall hook to print a funding message. It is the clearest illustration of why a default that runs arbitrary code for every dependency was always a strange bargain.

The packages everyone blames no longer do this

The tooling-heavy tree was built specifically to catch the usual suspects: Playwright for browser tests, Storybook for component development, Vitest, ESLint, Prettier. Between them they contributed 997 packages.

@playwright/test 1.62.1 declares no install script. Neither does any @storybook/* package at version 10.6.0. Playwright downloads its browsers through an explicit playwright install command you run yourself, not through a lifecycle hook. The reputation is several major versions out of date, and it is worth checking before you plan a migration around it.

What npm 12 actually switches on

I installed npm 12.0.2 and read its defaults rather than trusting the write-ups. npm 12.0.0 was published on 8 July 2026; 12.0.2, the current release, on 29 July 2026. The machinery landed earlier, in npm 11.16.0 on 27 May 2026, so an up-to-date npm 11 warns you before npm 12 blocks you.

SettingDefault in npm 12.0.2What it means
allow-scripts[""]An empty allowlist. No dependency install script runs until you add it.
allow-scripts-pintrueApprovals are written pinned to the version you reviewed, such as esbuild@0.28.2.
dangerously-allow-all-scriptsfalseThe escape hatch, named to discourage you.
allow-git"none"Git dependencies are blocked as well.
allow-remote"none"So are remote tarball sources.
min-release-agenullOff. The release-age hold is available, not automatic.
ignore-scriptsfalseUnchanged. The blocking is done by the new allowlist, not by this flag.

Two of those deserve attention. min-release-age being null means upgrading to npm 12 does not give you a cooldown on freshly published versions — that remains a setting you turn on deliberately, which I covered in what npm 12 changed and why. And ignore-scripts still defaulting to false catches people out: the old flag is not how this works now, so scripts you rely on can stop running while a config check tells you scripts are enabled.

How to measure your own tree before you upgrade

You do not need npm 12 for this, and you do not need to install anything. Any lockfile at lockfileVersion 2 or 3 already holds the answer.

  1. Run the count against your lockfile. From the project root:

    python3 -c "
    import json
    p = json.load(open('package-lock.json'))['packages']
    pkgs = {k: v for k, v in p.items() if k.startswith('node_modules/')}
    s = {k: v for k, v in pkgs.items() if v.get('hasInstallScript')}
    print(len(s), 'of', len(pkgs), 'packages declare an install script')
    for k, v in sorted(s.items()):
        print(' ', k.split('node_modules/')[-1], v.get('version'))
    "
    
  2. Cross off anything platform-restricted. If fsevents is in your list and you build on Linux, it is not in your migration. Check the package's os field rather than assuming.

  3. Justify each survivor in one sentence. For every package left, write down what its script does and why the build needs it. If you cannot finish the sentence, that package is a question for whoever added the dependency, not a line for the allowlist.

  4. Upgrade to npm 11.16.0 or later first. On that version the warnings appear while installs still succeed, so the breakage arrives as a message rather than a red build.

  5. Approve deliberately, in one commit. On npm 12, npm install-scripts ls lists what was blocked and npm install-scripts approve <pkg> writes the entry. Approve packages individually. --all exists, and it converts a review into a rubber stamp.

  6. Commit allowScripts and review it like code. It lives in package.json, so it shows up in diffs. A new entry appearing in a pull request is exactly the signal you want, and it is the only part of this that keeps paying after the upgrade.

To show the size of the result: the tRPC and Drizzle project above predicted three blocked scripts on Linux. Installing it under npm 12.0.2 blocked exactly three, all esbuild. One npm install-scripts approve esbuild produced the whole migration:

"allowScripts": {
  "esbuild@0.18.20": true,
  "esbuild@0.25.12": true,
  "esbuild@0.28.2": true
}

Common questions

How many npm packages actually run install scripts? In this sample, 22 of 3,602 — 0.61%. Excluding the macOS-only fsevents, 13 of 3,602, or 0.36%. Per project the count ranged from zero to five with a median of two, and it did not grow with the size of the dependency tree.

Is that different from how many packages on the npm registry have install scripts? Yes, and lower. Zahan et al. measured roughly 2.2% of npm packages registry-wide in 2022. Real application trees skew toward popular, well-maintained packages, and those are the ones that have moved away from install-time hooks.

Will npm 12 break my build? Only where a blocked script was doing real work, such as sharp, esbuild, or Prisma's engines. Measure first: the list is knowable from your committed lockfile today, so the upgrade does not have to be the thing that tells you.

Can I just set dangerously-allow-all-scripts in npm 12? You can, and you would be restoring the exact default that npm removed after a year of worm activity. If you need a temporary unblock, prefer approving the specific packages, because that leaves a reviewable record instead of a global opt-out nobody revisits.

Does the npm 12 install-script change apply to pnpm, Yarn, or Bun? All four package managers now have some form of install-script gate and release-age setting, with different names and defaults. The comparison across them is in what npm 12 changed and why; this post measures npm specifically, because npm is the one that records hasInstallScript in the lockfile.

The honest summary

Eleven trees is a sample, not a survey, and I am not going to claim it describes the ecosystem. What it does show is that for the stacks most teams actually ship, the blast radius of npm's new default is small, boring, and dominated by four packages: esbuild, sharp, Prisma's engines, and a macOS file watcher that never installs on your CI box anyway.

That cuts both ways. The migration is not the crisis it has been written up as. It is also not optional work you can skip, because the handful of packages that do run scripts are the ones that make the build work at all, and finding that out during an upgrade is the expensive version of finding it out.

The part worth keeping is not the allowlist. It is that allowScripts turns an invisible default into a file in your repository that changes in pull requests. A new package asking to execute code at install time now has to be approved by a person, once, in a diff. That is a better arrangement than the one we had, and it costs about five minutes.

If dependency risk in your codebase belongs to nobody in particular, web architecture consulting is where I take that on, and for a smaller site the equivalent is a maintained website care plan. For the neighbouring problems, rules for AI agents in a production codebase covers dependencies you did not choose, and shipping MCP in production covers the same lesson one layer up.

Sources: npm-install-scripts (npm CLI v12 docs) · package-lock.json and hasInstallScript (npm CLI v12 docs) · npm release history (npm registry) · Zahan et al., What are Weak Links in the npm Supply Chain? (2022) · npm v12: from implicit to explicit trust (JFrog)


Written by Jagatjeet — Jagatjeet (jagatjeet.com) is a web design, local SEO and AI automation studio in Kamloops, British Columbia, serving the Thompson-Okanagan and BC Interior. Published 6 September 2026. Last updated 6 September 2026. All package counts, install-script listings and npm defaults in this post were measured on 4 September 2026 against the live npm registry and a locally installed npm 12.0.2; the full method, stack manifests and reproduction script are in this site's research notes. Package versions and package manager defaults change quickly — rerun the measurement on your own lockfile rather than relying on these figures later.

New posts by email

Local SEO, web design, and digital marketing for BC Interior businesses. When a new post publishes — not on a schedule.

Apply

If this maps to a problem you're working on.

I work with $1M–$20M ARR founders whose digital investment isn't producing the return it should. Applications reviewed personally within 48 hours.

2 Diagnostic slots / month · 2–3 full engagements / quarter · 48h review