Guides

What runs when you npm install a package

Kenwea Protocol,

Installing an npm package can run code on your machine before you have imported a single line of it. That code comes from lifecycle scripts the package declares in its package.json. This guide covers which scripts run, how to see them in advance, what each package manager does by default, and where reading the manifest stops being enough.

The three scripts that run at install

When npm installs a package as a dependency, it runs that package's preinstall, install and postinstall scripts if they exist, in that order. Other entries in the scripts field, such as test, build or lint, do not run on install. They are for the package's own authors.

Legitimate packages use install scripts for real reasons: downloading or compiling a native binary, checking the platform, or printing a notice. esbuild is a well known example. Its postinstall runs node install.js to make sure the right platform binary is present.

See the scripts before you install

npm can show you a package's scripts without installing it:

npm view esbuild@0.28.2 scripts
# { postinstall: 'node install.js' }

npm view debug@4.4.3 scripts
# lint, test, test:node, test:browser, test:coverage: none of these run on install

Or ask the registry directly, which works from any language and in CI:

curl -s https://registry.npmjs.org/esbuild/0.28.2 | jq .scripts

This shows the package's own scripts only. npm runs install scripts for every package in the resolved dependency tree, so a package with no scripts can still run code at install through a dependency. Checking the tree means repeating this for each resolved package, which is what your lockfile lists.

What each package manager does by default

  • npm runs dependency install scripts by default. npm install --ignore-scripts skips them for one install, and npm config set ignore-scripts true makes that the default for your user.
  • pnpm, from version 10, does not run dependency install scripts by default. Packages that need them have to be allowed explicitly; pnpm approve-builds walks you through that and records the allowlist.
  • Bun runs install scripts only for packages you list in trustedDependencies in package.json, plus a built-in list of popular packages it trusts by default.

Turning scripts off is a reasonable default, but it has a cost: packages that build or download a native binary in their script may not work until you allow it. The useful question is then not "does it have a script" but "what does that script do".

Reading a script is not the same as knowing what it does

In a sample of MCP server packages we checked, four declared preinstall: npx only-allow pnpm. It reads like a harmless guard that stops you from using the wrong package manager, and that is its intent. But npx fetches only-allow from the registry when it is not cached, so this guard makes a network request at install time. Nothing about it is malicious. It is just something you cannot learn from the text of the script.

The same goes for node install.js: the manifest names a file, and the file can do anything. To know what a script does, you have to run it somewhere it cannot hurt you and watch.

Running the scripts in a sealed container

Kenwea's notary does that for any npm package, with no account or key. It fetches the exact tarball npm would install, runs the package's declared install scripts in a container with no network, all Linux capabilities dropped and a read-only filesystem, and returns a verdict signed under a published key.

npx -y @kenwea/mcp check esbuild

For esbuild 0.28.2, on 30 September 2026, the postinstall ran and exited with code 1, because the check does not install dependencies and esbuild's script looks for its platform binary in an optional dependency. The notary reports that as manual_review, not rejected: a script that fails for our reason is not evidence against the package. A package with no install scripts, such as debug, also gets manual_review, because nothing ran, and something that never ran has not passed.

  • approved means the scripts ran under those constraints and exited cleanly. It is not a statement that the package is good or safe for your use.
  • Dependencies are not installed, so the check covers a package's own install surface, not its whole tree.
  • What a module does when your code imports it is out of reach of an install-time check.

The verdict is bound to the sha256 of the bytes that were checked, so you can confirm it describes the tarball you have. The guide on verifying a signed verdict shows how, with your own code.

Try the notary with no account or key, or add it to your agent as an MCP server.

More guides