The tiny prepublish check that stopped me from accidentally publishing private npm packages

How a 40‑line prepublish check saved me from leaking proprietary code, what it looks for, and the real tradeoffs (including the day it blocked a legit release).

Written by: Arjun Malhotra

Close-up of hands typing on a laptop keyboard with code visible on the screen
Photo by Glenn Carstens-Peters on Unsplash

It was 10:27pm, I was tired, and my fingers still remembered last week’s publish flow. I ran npm publish from the wrong folder — one with an internal customer-facing package — and by the time I realised what I’d done, the package was already public.

Panic follows the first few seconds. Then comes practical work: yank the version, yank the package from the registry, update the README to “Not for public use”, ping the infra team to block downloads, and apologise. I promised myself two things that night: 1) never publish without a checklist; 2) automate the checklist so my tired brain couldn’t override it.

What I broke — and why it was easy to break I didn’t discover a mysterious npm loophole. I simply had:

npm publish is quick and forgiving. That’s the feature. It’s also why accidents happen.

What I actually built (short) I added a prepublishOnly hook that runs a tiny Node script. It checks three things locally before allowing an npm publish:

If any check fails, the script exits non‑zero and prints an actionable error with the fix.

Why I picked those checks We had three recurring mistakes:

  1. Publishing from wrong registry (local npmrc gets changed often).
  2. Publishing a package that shouldn’t be public (no @scope, private:false).
  3. Publishing from a half-finished branch because I forgot to switch to main/release.

These are easy to test from the local environment and cheap to surface to the developer. They don’t replace CI policy but they block the most common human errors.

The script, in practice No fluff here—my prepublishOnly is a 40‑line script (Node) that:

I added “prepublishOnly”: “node ./scripts/prepublish-check.js” to package.json. Works with npm, yarn, and pnpm because they call the same lifecycle scripts.

The day it failed me (the honest failure) A month later I hit my own safety net — in the worst possible moment. Our CI pipeline published a hotfix after we merged a critical change. CI runs in a container with a minimal npm config. My check expected NPM_CONFIG_REGISTRY to be set to our Verdaccio URL; the CI had the registry set via .npmrc in the repo, not as an env var, so npm config get registry returned the public registry and the script blocked the publish.

Result: failed release job, 45 minutes of digging, an angry PM. I patched the script to fall back to reading ~/.npmrc and project .npmrc, then to consult an explicit CI_ALLOW_PUBLISH env var. The takeaway: local checks are only as reliable as the environment they run in. They can introduce false negatives.

Why this still works (and what I enforce in CI) I now treat the local prepublish hook as the first line of defence. The second line is a server-side check:

If you can’t change your company’s registry rules, at least add the server-side gate in CI — that’s non-negotiable.

Tradeoffs I accepted

Small, concrete wins

Implementation notes you can copy

A final, slightly embarrassing thought The first time I fixed the script I was smug. Then CI taught me humility. Safety nets are great until they interrupt your release on a Friday. So build them, but expect them to be wrong occasionally, and make them explain themselves plainly.

Takeaway If you publish npm packages from a laptop in India (or anywhere), the real win is a small, local safety check that catches the dumb mistakes you make when tired — not a perfect, unbypassable fortress. Pair it with a CI gate and clear error messages, and you’ll save nights of apology emails.