Harden `npm install` in 2026: a checklist and an install-script scanner

Short answer: most of the hardening is a handful of settings: do not run install scripts you did not choose to run, install only from the lockfile, delay brand-new releases, refuse git dependencies you did not expect, and keep secrets out of the place where installs happen. The scanner linked below tells you which packages in your lockfile run install scripts, so the first step takes a minute.

This is a checklist, not a guarantee. It reduces how often a compromised package reaches you and how much it can take when it does; for background see why a postinstall script can read your secrets. Settings below were checked on 2026-11-01, mostly by running them; where we only read the documentation we say so.

Step 0: see what runs on install

Paste your package-lock.json into the install-script scanner (it runs in your browser; nothing is sent). It reads npm’s own hasInstallScript marker and also lists git, URL and file dependencies and registry entries without an integrity hash. On a lockfile for esbuild, left-pad and husky it reported exactly one package with an install script: [email protected]. (husky’s hook setup uses a prepare script, which is a different marker.) The same list from a shell:

jq -r '.packages | to_entries[] | select(.value.hasInstallScript) | .key + "@" + .value.version' package-lock.json

Most of what you find will be legitimate: native builds, binary downloads. The point is that you now know the list, and can decide which packages should be allowed to run code.

The checklist

Scripts

  • Install from the lockfile, with scripts off, by default. In CI: npm ci --ignore-scripts. Or set it permanently: ignore-scripts=true in .npmrc. Then run only what you need: npm rebuild <package> runs a package’s install scripts on demand. Tradeoff: packages that need a build step won’t work until you do.
  • Know your package manager’s default. From their documentation: Yarn (Berry) has enableScripts defaulting to false, so it does not run third-party postinstall scripts; Bun is “default-secure” and runs lifecycle scripts only for packages in trustedDependencies; npm runs scripts by default. pnpm has ignoreScripts and build-allow settings in its settings reference; read how your version treats dependency builds.
  • A script that exists is not a script that is malicious. Review the ones you don’t recognise; allow the ones you need by name.

Time

  • Delay new versions. npm’s min-release-age (days) only considers versions published longer ago than that. We ran it: with --min-release-age=7 an install resolved normally; with an absurd 36500 days it failed with ENOVERSIONS (“No versions available for left-pad”), which is what a too-strict setting looks like. It cannot be combined with --before. Equivalents exist elsewhere: pnpm’s minimumReleaseAge and Yarn’s npmMinimalAgeGate (both in their docs; we did not run them). A delay gives scanners and registries time to flag a bad release; it is not a guarantee, and it does nothing for versions you already depend on.
  • Update on purpose. Pin the dependency set (lockfile committed, npm ci in CI) and update by pull request, so a new release reaches you when you decide.

Sources

  • Refuse git dependencies you did not choose. npm’s allow-git setting takes all, none or root. With allow-git=none, a project with a github: dependency failed to install: Fetching packages of type "git" have been disabled / Refusing to fetch .... We tested none only; read the npm docs for what root allows.
  • Check the lockfile diff in review. New resolved hosts, entries without integrity, or a package that suddenly has an install script are the lines to look at. The scanner flags the first two and the third.
  • Use a private registry mirror or proxy if you can, to control what reaches builds.

Environment

  • Install where there are no secrets. The loot in every npm worm has been the credentials in the install environment (evidence). Build jobs shouldn’t hold deploy keys; use short-lived credentials (OIDC) and step-level environment variables (GitHub Actions).
  • Keep secrets out of files an install can read: .env, ~/.npmrc tokens with publish rights, cloud credentials (migration).
  • Enforce phishing-resistant MFA on npm and GitHub for anyone who can publish; CISA’s guidance on the 2025 worm lists it (CISA).

Runtime and response

  • Limit what a service can execute and connect to, knowing the limits: the process firewall refuses direct execution and connections you did not allow, but does not contain code inside an allowed runtime.
  • Monitor what is installed in production and get alerted on new advisories and known-malicious versions (how).
  • Have the rotation playbook ready before you need it (first hour, rotate everywhere).

What this does not cover

  • A malicious package that runs only at import time in your app is not stopped by --ignore-scripts; that is the runtime layers’ job.
  • min-release-age and similar delays help only if the bad release is noticed within the window.
  • The scanner reads npm lockfiles (version 2 and 3). It does not read pnpm, Yarn or Bun lockfiles, and it cannot tell you whether a script is malicious.

Facts last verified . Spotted an error? Tell us.

New here? Start here for the guides in a sensible order.