npm lifecycle hooks in execution order
npm runs a defined set of lifecycle scripts at specific moments during install, pack, publish, and version operations. Knowing the exact sequence — and which command triggers which hook — lets you place build steps, test guards, and release automation in the correct package.json field. Placing a script in the wrong hook means it either runs too early, runs on the wrong command, or silently never runs at all.
The four triggering commands
npm install
preinstall
→ dependency resolution and install
install
postinstall
prepare
prepare runs last, after all dependencies are installed. It is the right place for build steps that need dependencies available (like tsc or vite build) and that consumers of your package also need to trigger when they install it as a git dependency.
npm publish
prepublishOnly
prepack
prepare
→ tarball creation
postpack
publish
postpublish
prepublishOnly is publish-only, so it is the conventional location for tests and lint guards. prepare fires here too, which is why it must be idempotent — it will run on both install and publish.
npm pack (standalone)
prepack
prepare
→ tarball written
postpack
npm version
preversion
→ version bumped in package.json
version
→ git commit created
postversion
postversion is the standard place to run git push --follow-tags to push the version commit and the new tag to the remote.
The pre/post wrapper pattern
For any script named x, npm automatically looks for and runs prex before x and postx after it. This applies to built-in lifecycle names and your own custom scripts:
{
"scripts": {
"pretest": "node check-env.js",
"test": "jest",
"posttest": "node cleanup.js"
}
}
Custom scripts like build get prebuild and postbuild for free.
Practical placements
{
"scripts": {
"preinstall": "node check-node-version.js",
"prepare": "tsc -p tsconfig.build.json",
"prepublishOnly": "npm test && npm run lint",
"postversion": "git push --follow-tags"
}
}
preinstall— check Node or environment constraints before dependencies are pulled.prepare— compile TypeScript or run a bundler; runs on both install (git deps) and publish.prepublishOnly— the publish gate; never runs during installs, so safe for slow tests.postversion— push afternpm versionwithout manual steps.
pnpm differences
pnpm v7 and later ignores preinstall, install, and postinstall scripts of dependencies (not your own package) by default, for security reasons. If a dependency’s postinstall script is genuinely needed (for example to compile a native module), you must allow-list it in .npmrc with unsafe-perm or the allow-run-scripts setting.
Common mistakes
- Putting tests in
prepareinstead ofprepublishOnly—prepareruns on everynpm install, so tests block every install. Move them toprepublishOnly. - Forgetting that
prepareruns in git-dependency installs — if a downstream project installs your package from a git URL, npm runspreparein your package. This is intentional: it builds the package so downstream consumers get compiled output, not source. - Using
postinstallfor app setup in a deployed environment —postinstallfires for everynpm install, including fresh CI installs. A heavy postinstall step adds to every cold build. Consider moving one-time setup to an explicit script called by CI instead. - Expecting
prepublishto run only at publish —prepublishfires onnpm installtoo (an old npm 2 legacy). UseprepublishOnlyinstead for publish-only guards.