npm 11 🌐 Lifecycle Scripts (pre, post, prepare, prepublishOnly)
npm lifecycle scripts are hooks that run automatically at specific points in a package’s lifecycle—during install, pack, publish, or when a user-defined script is invoked. The pre and post prefixes are the general mechanism: any script named prefoo runs before foo, and postfoo runs after it . The special scripts prepare, prepublishOnly, and the deprecated prepublish are different: they are tied to npm’s own operations, not to user-defined script names, and their execution conditions are not obvious from their names. The confusion around these hooks is historical. prepublish originally ran on both npm publish and npm install, which was convenient but surprising. npm 4.0.0 introduced prepare to preserve the useful behavior and prepublishOnly to provide a publish-only hook, while deprecating prepublish .
This chapter covers the four lifecycle scripts named in the title, their exact execution conditions, the order in which they fire during npm publish, and the security implications of prepare running during git-dependency installation.
Key point: prepare runs on npm install in the package’s own directory and when installed as a git dependency, in addition to running before pack and publish. prepublishOnly runs only on npm publish. Choosing the wrong one means either your build does not run for contributors, or your publish-only checks run on every consumer’s install.
Why these lifecycle scripts exist
The build-before-use problem. A package written in TypeScript or another compiled language must be built before it can be consumed. The build must happen before npm pack creates a tarball, before npm publish uploads it, and after a fresh clone when a contributor runs npm install. A single hook that covers all three cases is what prepare provides . Without it, the build step would need to be duplicated in multiple scripts, each triggered by a different command.
The publish-safety problem. Some checks should run only when publishing: a full test suite, a lint pass, a verification that the version in package.json matches the git tag. These checks are too slow or too strict to run on every local install. prepublishOnly exists for this case. It fires exactly once per publish operation and nowhere else . Running the test suite in prepare would slow every npm install in the package’s own directory; running it in prepublishOnly keeps it confined to the publish workflow.
The historical confusion problem. Before npm 4.0.0, prepublish ran on both npm publish and npm install. This meant a package’s build step ran when consumers installed it from git, which was often desirable but occasionally destructive. It also meant there was no way to run a script only on publish. The npm team split the behavior: prepare inherited the broad execution, prepublishOnly was added for the narrow case, and prepublish was deprecated . Understanding this history explains why both hooks exist despite overlapping functionality.
The security problem. Lifecycle scripts execute arbitrary code during npm install. When a package is installed from a git URL and contains a prepare script, npm installs its dependencies and devDependencies and runs the script before packaging. This means a compromised or malicious transitive dependency can execute code on a developer’s machine or CI runner long before any import statement runs . The convenience of prepare for git dependencies is also its risk.
a. pre and post Scripts
The pre and post prefixes are not lifecycle scripts in the special sense. They are a naming convention that npm applies to any script defined in the scripts object. If package.json defines compress, precompress, and postcompress, then running npm run compress executes all three in order: precompress, then compress, then postcompress .
{
"scripts": {
"precompress": "echo 'Preparing to compress'",
"compress": "node scripts/compress.js",
"postcompress": "echo 'Compression complete'"
}
}
Running npm run compress executes the three scripts in sequence. The same pattern applies to the built-in scripts that npm recognizes: npm test runs pretest, test, and posttest; npm start runs prestart, start, and poststart . The prefix mechanism is general and applies to every script, including user-defined ones.
The distinction between pre/post and the special lifecycle scripts is one of trigger. precompress runs only when compress is explicitly invoked. prepare runs when npm install, npm pack, or npm publish is invoked, regardless of whether any script named prepare is otherwise referenced. The pre/post hooks are attached to a named script; the special lifecycle hooks are attached to an npm command.
b. prepare: The Broad Hook
prepare is the most widely triggered special lifecycle script. It runs in four situations: before the package is packed during npm pack, before the package is published during npm publish, on a local npm install run without arguments in the package’s own directory, and when the package is installed as a git dependency . The fourth case is the one that surprises people and the one that carries security implications.
{
"scripts": {
"prepare": "npm run build",
"build": "tsc -p tsconfig.build.json"
}
}
This configuration compiles TypeScript before packing and publishing, after a fresh clone and npm install in the package’s directory, and when a consumer installs the package from a git URL. The build step is defined once and triggered by every relevant operation. The alternative—duplicating the build command in prepack and in a post-install hook—is more fragile and easier to forget.
The git-dependency behavior deserves emphasis. When a consumer runs npm install git+https://github.com/user/repo.git, npm clones the repository, installs the package’s dependencies and devDependencies, runs prepare, and then packages the result. The consumer receives a built artifact, not raw source, which is the intended benefit. The cost is that the package’s entire dependency tree—including transitive devDependencies—executes on the consumer’s machine during install . A prepare script is therefore both a build convenience and an execution surface.
c. prepublishOnly: The Publish-Only Hook
prepublishOnly runs before the package is prepared and packed, only on npm publish . It does not run on npm install, npm pack, or git-dependency installation. It is the correct location for checks that should gate a release but should not burden local development or downstream consumers.
{
"scripts": {
"prepublishOnly": "npm test && npm run lint && node scripts/check-branch.js"
}
}
The script above runs the test suite, the linter, and a branch check only when publishing. A contributor running npm install after cloning the repository does not wait for the full test suite; a consumer installing from git does not run it either. The checks are confined to the moment they matter: the publish operation.
The ordering during npm publish is prepublishOnly, then prepare, then prepack, then the publish itself . This means prepublishOnly can fail the publish before any build work begins, which is the desired behavior for a gate. If the tests fail in prepublishOnly, the prepare build never runs.
Complete Example Session
// ============================================
// PART 1: A PACKAGE WITH ALL FOUR HOOKS
// ============================================
// package.json — demonstrates the distinct roles
{
"name": "example-package",
"version": "1.0.0",
"scripts": {
"prebuild": "echo 'Cleaning dist/' && rm -rf dist",
"build": "tsc -p tsconfig.build.json",
"postbuild": "echo 'Build complete'",
"pretest": "echo 'Preparing test environment'",
"test": "vitest run",
"posttest": "echo 'Tests complete'",
"prepare": "npm run build",
"prepublishOnly": "npm test && npm run lint"
}
}
// ============================================
// PART 2: PRE AND POST AROUND A USER SCRIPT
// ============================================
// Running `npm run build` executes:
// 1. prebuild → removes dist/
// 2. build → compiles TypeScript
// 3. postbuild → prints confirmation
// The pre/post hooks are attached to the named script.
// ============================================
// PART 3: PREPARE ON LOCAL INSTALL
// ============================================
// A contributor clones the repo and runs:
// $ npm install
// Because this is a local install without arguments,
// npm runs `prepare`, which runs `build`.
// The contributor gets a working build without a manual step.
// ============================================
// PART 4: PREPARE ON GIT DEPENDENCY INSTALL
// ============================================
// A consumer adds the package via git:
// $ npm install git+https://github.com/user/example-package.git
// npm clones the repo, installs dependencies and devDependencies,
// runs `prepare`, and packages the built result.
// The consumer receives compiled JavaScript, not TypeScript source.
// ============================================
// PART 5: PREPUBLISHONLY ON PUBLISH
// ============================================
// When the maintainer runs:
// $ npm publish
// The sequence is:
// 1. prepublishOnly → runs tests and lint
// 2. prepare → runs build
// 3. prepack → runs packing hooks (if defined)
// 4. publish → uploads tarball
// If prepublishOnly fails, nothing else runs.
// ============================================
// PART 6: THE DEPRECATED PREPUBLISH
// ============================================
// `prepublish` still runs on npm install and npm ci
// in some npm versions, but it is deprecated.
// Do not use it. Use `prepare` for install-time
// and `prepublishOnly` for publish-time.
// ============================================
// PART 7: SKIPPING LIFECYCLE SCRIPTS
// ============================================
// To install a package without running its scripts:
// $ npm install --ignore-scripts
// This is useful when auditing a suspicious package
// or installing from an untrusted git URL.
// ============================================
// PART 8: SECURITY CONSIDERATIONS
// ============================================
// `prepare` runs during git-dependency install.
// A malicious transitive dependency can execute
// arbitrary code at install time, before any import.
// Mitigations: prefer registry tarballs, use npm ci
// with a lockfile, audit scripts with --ignore-scripts.
// ============================================
// PART 9: ORDER OF OPERATIONS SUMMARY
// ============================================
// npm install (local): preinstall → install → postinstall
// → prepublish (deprecated)
// → preprepare → prepare → postprepare
// npm pack: prepack → prepare (if not already run) → postpack
// npm publish: prepublishOnly → prepare → prepack → publish → postpublish
// ============================================
// PART 10: CHOOSING THE RIGHT HOOK
// ============================================
// Need to build for contributors and consumers? → prepare
// Need a check only before publish? → prepublishOnly
// Need a hook before a named script? → pre<name>
// Need a hook after a named script? → post<name>
// Do not use prepublish. It is deprecated.
The ten parts trace the distinct execution conditions: pre/post around user scripts, prepare on local and git installs, prepublishOnly on publish, the deprecated prepublish, the --ignore-scripts escape hatch, and the order in which hooks fire during publish.
Quick Reference
Hook Execution Matrix
| Hook | npm install (local) | npm install (git dep) | npm pack | npm publish |
|---|---|---|---|---|
prepare | Yes | Yes | Yes | Yes |
prepublishOnly | No | No | No | Yes |
prepack | No | Yes | Yes | Yes |
prepublish (deprecated) | Yes (legacy) | No | No | No |
Order During npm publish
| Order | Script |
|---|---|
| 1 | prepublishOnly |
| 2 | prepare |
| 3 | prepack |
| 4 | (publish) |
| 5 | publish |
| 6 | postpublish |
Hook Purposes
| Hook | Purpose | Use When |
|---|---|---|
pre<name> | Run before named script | Cleanup, setup |
post<name> | Run after named script | Notifications, cleanup |
prepare | Build before use | Compile TS, bundle |
prepublishOnly | Gate publish | Tests, lint, branch check |
Best Practices
✅ Do This:
// Use prepare for build steps that must run for contributors
"prepare": "npm run build" // ✅
// Use prepublishOnly for release gates
"prepublishOnly": "npm test && npm run lint" // ✅
// Keep prepare minimal and auditable
"prepare": "tsc -p tsconfig.build.json" // ✅
# Audit packages before allowing scripts
npm install --ignore-scripts # ✅
// Use pre/post for script-specific setup and teardown
"prebuild": "rm -rf dist", "postbuild": "echo done" // ✅
❌ Don’t Do This:
// Use prepublish (deprecated) for new packages
"prepublish": "npm run build" // ❌
// Put publish-only tests in prepare (runs on every install)
"prepare": "npm test" // ❌
// Fetch and execute remote code in prepare
"prepare": "curl http://example.com/script.sh | sh" // ❌
# Install from git URLs without auditing scripts
npm install git+https://untrusted.example/repo.git // ❌
Common Pitfalls
| Pitfall | Why It Happens | Fix |
|---|---|---|
| Build runs on every consumer install | Build placed in prepare | Move to prepack or keep prepare minimal |
| Publish gate never runs | Used prepare instead of prepublishOnly | Use prepublishOnly for publish-only checks |
| Git install fails on build | prepare requires devDependencies not installed | Ensure devDependencies listed; use registry tarball |
| Malicious code runs at install | prepare in git dependency executes | Audit with --ignore-scripts; prefer registry |
| Deprecated hook triggers unexpectedly | prepublish still runs on npm ci | Remove prepublish; use prepare |
Real-World Examples
1. Build on Prepare
"prepare": "npm run build"
2. Test Gate on Publish
"prepublishOnly": "npm test && npm run lint"
3. Pre/Post Around Custom Script
"precompress": "rm -rf dist", "compress": "node compress.js", "postcompress": "echo done"
4. Pre/Post Around Test
"pretest": "npm run lint", "test": "vitest", "posttest": "echo done"
5. Skip Scripts on Install
npm install --ignore-scripts
6. Audit a Git Dependency
npm install --ignore-scripts git+https://example.com/repo.git
7. Minimal Prepare for TypeScript
"prepare": "tsc -p tsconfig.build.json"
8. Branch Check on Publish
"prepublishOnly": "node scripts/check-branch.js"
9. Prepack for Tarball-Specific Work
"prepack": "npm run build"
10. Postpublish Notification
"postpublish": "node scripts/notify.js"
Visual
Hook Execution Conditions
┌──────────────────────────────────────────────────────────────┐
│ COMMAND HOOKS THAT RUN │
│ │
│ npm install (local) preinstall → install │
│ → postinstall │
│ → prepare │
│ │
│ npm install (git dep) (in cloned repo) │
│ preinstall → install │
│ → postinstall │
│ → prepare │
│ │
│ npm pack prepack → prepare │
│ │
│ npm publish prepublishOnly → prepare │
│ → prepack → publish │
│ → postpublish │
│ │
│ prepare runs in FOUR cases. │
│ prepublishOnly runs in ONE case. │
│ │
└──────────────────────────────────────────────────────────────┘
The Publish Sequence
┌──────────────────────────────────────────────────────────────┐
│ npm publish │
│ │
│ ┌─────────────────┐ │
│ │ prepublishOnly │ Tests, lint, branch check │
│ │ │ FAIL → stop, nothing else runs │
│ └────────┬────────┘ │
│ ▼ │
│ ┌─────────────────┐ │
│ │ prepare │ Build (compile TS, bundle) │
│ └────────┬────────┘ │
│ ▼ │
│ ┌─────────────────┐ │
│ │ prepack │ Tarball-specific preparation │
│ └────────┬────────┘ │
│ ▼ │
│ ┌─────────────────┐ │
│ │ publish │ Upload to registry │
│ └────────┬────────┘ │
│ ▼ │
│ ┌─────────────────┐ │
│ │ postpublish │ Notifications, cleanup │
│ └─────────────────┘ │
│ │
│ prepublishOnly is the gate. prepare is the build. │
│ │
└──────────────────────────────────────────────────────────────┘
Prepare and Git Dependencies
┌──────────────────────────────────────────────────────────────┐
│ CONSUMER INSTALLS FROM GIT │
│ │
│ npm install git+https://github.com/user/repo.git │
│ │
│ ┌─────────────────┐ │
│ │ Clone repository│ │
│ └────────┬────────┘ │
│ ▼ │
│ ┌─────────────────┐ │
│ │ Install deps │ Including devDependencies │
│ │ and devDeps │ │
│ └────────┬────────┘ │
│ ▼ │
│ ┌─────────────────┐ │
│ │ Run prepare │ Build runs on consumer's machine │
│ └────────┬────────┘ │
│ ▼ │
│ ┌─────────────────┐ │
│ │ Package and │ Consumer gets built artifact │
│ │ install │ │
│ └─────────────────┘ │
│ │
│ A malicious transitive devDependency can execute here. │
│ Mitigation: --ignore-scripts or registry tarball install. │
│ │
└──────────────────────────────────────────────────────────────┘
Pre/Post Naming Convention
┌──────────────────────────────────────────────────────────────┐
│ package.json scripts: │
│ │
│ "prebuild" → runs before "build" │
│ "build" → the named script │
│ "postbuild" → runs after "build" │
│ │
│ npm run build executes: │
│ 1. prebuild │
│ 2. build │
│ 3. postbuild │
│ │
│ The prefix is a naming convention. │
│ It applies to ANY script name, including: │
│ pretest/test/posttest │
│ prestart/start/poststart │
│ precompress/compress/postcompress │
│ │
│ These are NOT the special lifecycle hooks. │
│ They run only when the named script is invoked. │
│ │
└──────────────────────────────────────────────────────────────┘
Summary
| Item | Value |
|---|---|
pre<name> | Runs before named script; naming convention |
post<name> | Runs after named script; naming convention |
prepare | Build before pack, publish, local install, git install |
prepublishOnly | Gate before publish only |
prepublish | Deprecated; do not use |
| Publish order | prepublishOnly → prepare → prepack → publish |
| Git dependency risk | prepare executes on consumer’s machine |
| Skip scripts | npm install --ignore-scripts |
Key takeaways:
prepareruns in four situations. Before pack, before publish, on local install without arguments, and on git-dependency install . The last case is the one that carries security implications and the one most often overlooked.prepublishOnlyruns in exactly one situation. Only onnpm publish. It is the correct location for test suites, linters, and branch checks that should gate a release but should not run on every install.preandpostare not special lifecycle hooks. They are a naming convention attached to user-defined and built-in script names.precompressruns only whencompressis invoked, not on any npm command .- The publish sequence is ordered.
prepublishOnlyruns beforeprepare, which runs beforeprepack. A failure inprepublishOnlyprevents the build from running . prepareis an execution surface. A package installed from git runs itspreparescript on the consumer’s machine, after installing its full dependency tree. This is convenient for maintainers and risky for consumers .prepublishis deprecated. It still runs onnpm ciandnpm installin some versions, but new packages should usepreparefor install-time work andprepublishOnlyfor publish-time work .
Remember: The choice between prepare and prepublishOnly is not a matter of preference. It is a decision about who runs your code and when. prepare runs for contributors after a clone, for consumers who install from git, and for the publish pipeline. prepublishOnly runs only for the maintainer at the moment of release. Build steps belong in prepare because they must run before the package is used. Release gates belong in prepublishOnly because they should run only when a release is being cut. Putting a test suite in prepare slows every install; putting a build in prepublishOnly means git-dependency consumers receive uncompiled source. The deprecated prepublish exists only for backward compatibility and should not appear in new packages.
Stop using slow, ad-bloated tool sites! 🤮
🔎 Search “KandZ Tools” on Google to use many professional utilities for free.
KandZ.me is the ultimate minimalist hub for:
✅ Finance (Mortgage, Interest, Inflation)
✅ Tech (Base64, JSON, Dev Suite, IP)
✅ Health (BMI, BMR, TDEE)
✅ Productivity (Timer, Workspace, QR)
⚡️ Fast & Private
🔒 No data leaves your device
💎 100% Free
🔗 Use it now: https://tools.kandz.me
🔖 Bookmark it—you’ll need it later!