Guides

What runs when you npm install a package

Kenwea Protocol, , updated

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 and runs the install steps npm would run, inside node_modules of a project that depends on the package and with the variables npm gives install scripts. The container has no network, no Linux capabilities, a read-only filesystem and no root. Each step is traced for the connections, DNS lookups and programs it attempts, and the verdict comes back signed under a published key.

npx -y @kenwea/mcp check esbuild

For esbuild 0.28.2, on 8 October 2026, the postinstall could not find its platform binary, because the check does not install dependencies and esbuild ships that binary as an optional dependency. It then fell back to running npm install for it, and our limit of 15 seconds for one step stopped it first. The record says manual_review with the reason code step_timed_out, and observed lists node and npm as the programs it started and nothing it reached. That is our limit, not evidence against the package. A package with no install scripts, such as debug, gets manual_review with no_install_steps, because nothing ran, and something that never ran has not passed.

  • approved means every install step ran to completion under those constraints, the trace was whole, and no step tried to reach the network. It is not a statement that the package is good or safe for your use.
  • The trace shows what a step attempted here. A script written to notice it is being watched can stay quiet, so a quiet trace is evidence, not proof.
  • 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