Safeguard
Open Source

The npm prepare Script: What It Does and How to Use It Safely

The npm prepare lifecycle script runs at more moments than most developers realize, including when someone installs your package from git. Here is exactly when it fires and how to keep it from becoming an attack vector.

Marcus Chen
DevSecOps Engineer
6 min read

The npm prepare script is a lifecycle hook that runs before your package is packed or published, on a local npm install with no arguments, and, importantly, when your package is installed as a git dependency. That last case is the one that surprises people and the one that matters most for security, because it means arbitrary build code from a git-hosted package can execute on a consumer's machine during install.

If you maintain a package, prepare is where you compile TypeScript, bundle assets, or generate files before publishing. If you consume packages, prepare is one of the lifecycle hooks that can run code you did not write. Both perspectives are worth understanding.

When the prepare script actually runs

According to the npm documentation, prepare fires in several distinct situations:

  1. Before the package is packed, during npm pack and npm publish. This is the build-before-ship use case.
  2. On a local npm install run with no arguments, in the package's own directory. This lets contributors get a working build right after cloning.
  3. When the package is installed as a git dependency. If a consumer adds your package via a git URL and your package has a prepare script, npm installs your dependencies and devDependencies and runs prepare before packaging and installing.
  4. When installed as a local link through npm install <folder>.

It runs after prepublish and before prepublishOnly and prepack in the publish sequence. The rule of thumb from the docs: put platform-independent build steps in prepare, and use it for tasks that must happen once, in a single place.

A common, legitimate prepare looks like this:

{
  "scripts": {
    "build": "tsc -p tsconfig.build.json",
    "prepare": "npm run build"
  }
}

This compiles the source before publish and after a fresh clone, so consumers of the published tarball receive compiled JavaScript rather than raw TypeScript.

prepare versus prepublishOnly versus prepack

These three hooks overlap enough to confuse. The key differences:

  • prepare runs on pack, publish, local install, and git-dependency install. Broadest reach.
  • prepack runs before a tarball is packed, on npm pack, npm publish, and git-dependency installs, but not on a plain local npm install.
  • prepublishOnly runs only on npm publish. Use it for checks you want exclusively at publish time, such as running the full test suite or refusing to publish from the wrong branch.

If you want a step to run for anyone building from source, prepare is right. If you want it strictly at publish, prepublishOnly is the safer choice because it will not fire on a consumer's git install.

The security problem with lifecycle scripts

Here is the uncomfortable part. Lifecycle scripts, including prepare, preinstall, install, and postinstall, execute during npm install. That install typically happens on developer laptops and CI runners that hold source code, environment secrets, cloud credentials, and network access. A package that runs malicious code in one of these hooks has all of that within reach.

The git-dependency behavior makes prepare especially notable. When you install from a git URL, npm does not just download a prebuilt tarball; it clones the repo, installs its full dependency and devDependency tree, and runs prepare. That is a large amount of third-party code executing before you have run a single line of your own program. A compromised or malicious dependency deep in that tree can do damage at install time, long before any test or scan looks at the code.

This is the mechanism behind a whole class of supply chain incidents. The malicious payload does not wait to be imported and called; it runs the moment you install.

How to use prepare safely

For maintainers:

  • Keep prepare scripts minimal and auditable. A single npm run build is easy to reason about; a chain that fetches remote scripts and pipes them to a shell is not.
  • Avoid fetching and executing remote code during prepare. Anything you download at build time is code you cannot review at review time.
  • Prefer publishing prebuilt artifacts so consumers install a tarball, not a git checkout that has to build itself.

For consumers:

  • Prefer versioned registry installs over git URLs. A published 1.4.2 from the registry does not run prepare on install the way a git dependency does.
  • Use npm ci with a committed lockfile so you install exactly the versions you vetted.
  • When you must audit install behavior, npm install --ignore-scripts skips lifecycle scripts entirely, letting you inspect a package before allowing it to run anything.
  • Run software composition analysis on your tree so known-malicious or known-vulnerable packages get flagged before they reach an install. An SCA tool can surface transitive risk that a manual read of your direct dependencies would miss.

If you want the bigger picture on how one line in package.json pulls in a web of trusted-by-default code, read what is a dependency in programming.

A quick decision guide

Ask yourself what the script needs to do and when:

  • Build step everyone needs, including source consumers? Use prepare.
  • Publish-only guard rail, like tests or branch checks? Use prepublishOnly.
  • Packing logic that should not run on a plain local install? Use prepack.
  • Anything that reaches out to the network at install time? Reconsider, and move it out of a lifecycle hook if you can.

FAQ

Does npm prepare run on npm install for consumers?

Not for a normal registry install of a versioned package. It runs on a local npm install in the package's own directory, and it runs when the package is installed as a git or local-folder dependency. A standard npm install some-package@1.2.3 from the registry does not trigger the published package's prepare.

What is the difference between prepare and postinstall?

prepare is aimed at build steps before packing or publishing and on git installs. postinstall runs after a package is installed and is frequently the hook abused in supply chain attacks. Both can execute arbitrary code, so both deserve scrutiny.

How do I install a package without running its scripts?

Use npm install --ignore-scripts. This skips all lifecycle hooks, including prepare and postinstall, which is useful when you want to inspect a package before letting it run anything on your machine.

Why is the git-dependency behavior a security concern?

Installing from a git URL causes npm to clone the repo, install its full dependency and devDependency tree, and run prepare, all before the code is used. That is a lot of third-party code executing at install time, which is exactly where install-time attacks land.

Never miss an update

Weekly insights on software supply chain security, delivered to your inbox.