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:
- Before the package is packed, during
npm packandnpm publish. This is the build-before-ship use case. - On a local
npm installrun with no arguments, in the package's own directory. This lets contributors get a working build right after cloning. - When the package is installed as a git dependency. If a consumer adds your package via a git URL and your package has a
preparescript, npm installs your dependencies and devDependencies and runspreparebefore packaging and installing. - 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 localnpm 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
preparescripts minimal and auditable. A singlenpm run buildis 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.2from the registry does not runprepareon install the way a git dependency does. - Use
npm ciwith a committed lockfile so you install exactly the versions you vetted. - When you must audit install behavior,
npm install --ignore-scriptsskips 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.