package.json: Not just a file, but a Developer's Toolkit

The package.json scripts block is the one configuration surface every Node project agrees on. It's also where most projects accumulate the most rot — half the entries are stale, half rely on shell behavior that breaks on Windows. Here are the patterns worth knowing in 2026.

Pre and post hooks

Any script foo automatically picks up a prefoo and postfoo if they exist. Useful for bracketing housekeeping around a primary task:

1"scripts": { 2 "pretest": "eslint .", 3 "test": "jest", 4 "posttest": "rimraf ./coverage" 5}

Use rimraf rather than rm -rf if you want this to work on Windows.

Reading metadata from scripts

process.env.npm_package_* exposes top-level fields from package.jsonname, version, and friends. It still works in npm 10/11, though it's effectively undocumented as stable. Don't rely on nested fields; only the top-level ones are dependable.

1"scripts": { 2 "start": "node index.js", 3 "info": "echo The current app is $npm_package_name@$npm_package_version" 4}

$npm_package_name is shell-syntax — it works in Bash/Zsh, not in Windows cmd. For cross-shell, wrap the script in a tiny Node one-liner or in cross-env.

Cross-platform compatibility

cross-env remains the canonical way to set environment variables portably:

1"scripts": { 2 "start": "cross-env NODE_ENV=development node index.js" 3}

Node and Bun now both have --env-file for loading .env files, but for inline KEY=value command syntax that works on Windows, cross-env is still the path of least resistance.

Concurrent and serial composition

For running scripts in parallel or sequence, concurrently is the reliable pick. It supports name-based wildcards (npm:start:* expands to every script matching the prefix; requires npm 7+):

1"scripts": { 2 "start:server": "node server.js", 3 "start:watch": "nodemon .", 4 "start": "concurrently npm:start:*" 5}

npm-run-all was the other classic option, but it hasn't seen a release since 2022 — use the maintained fork npm-run-all2 if you prefer its API.

Shell operators && (sequential) and & (background, on Unix) work too, but & is sequential on Windows cmd, so leaning on it ships non-portable scripts. concurrently papers over that.

Lifecycle scripts that actually fire

The lifecycle hooks that still matter:

  • prepare — runs on npm install (when invoked locally) and on npm publish. Useful for husky setup and TypeScript builds in libraries.
  • prepublishOnly — runs only on npm publish, never on npm install. Use this for publish-time checks that shouldn't run for consumers.
  • postinstall — runs after install in any context.

prepublish is deprecated and has been a no-op since npm 7. Don't reach for it.

Security: postinstall is a footgun in 2026

preinstall and postinstall scripts run automatically and have full shell access. The 2024–2025 supply-chain attacks (compromised packages running malicious postinstall) have made this a regularly exploited path. Two ways to limit blast radius:

1# Skip lifecycle scripts globally for one install 2npm install --ignore-scripts 3 4# Or disable them by default in ~/.npmrc 5echo "ignore-scripts=true" >> ~/.npmrc

Bun goes further: it ignores lifecycle scripts on non-trusted dependencies by default. pnpm has onlyBuiltDependencies for the same purpose.

packageManager + Corepack

The packageManager field pins which package manager (and version) the project expects:

1"packageManager": "[email protected]"

With Corepack enabled (default in some Node distributions), running npm / yarn / pnpm in the project directory transparently uses the version declared here. No more "works on my machine" because someone had a different pnpm version installed globally.

Wait, what about description for scripts?

There isn't one. npm has never shipped a per-script description field, despite the rumour. The closest patterns:

  • A sibling // key in the scripts block (npm ignores keys starting with //):
    1"scripts": { 2 "//build": "Compile TypeScript and bundle assets", 3 "build": "tsc && vite build" 4}
  • Tools like ntl or better-scripts for richer interactive script docs.

Neither is built in. If discoverability really matters, document scripts in the README.