Free guide · Francisco Arrieta · 3 min

Block bad pushes with a git hook

Wire a pre-push git hook that runs knip and blocks the push when it finds dead code

Dead code doesn’t get deleted because nobody notices it’s there. A hook that fails the push when it finds some means it gets caught before it ever reaches the repo.

Unused exports, orphaned files, dependencies nobody imports anymore. They pile up quietly in every JavaScript project, because deleting code feels riskier than leaving it. This wires a check into the moment you push, so the question gets asked automatically instead of never.

What you’ll need

  • A JavaScript or TypeScript project with a git repo
  • Node.js installed
  • About 15 minutes

Step 1

See what Knip finds, before automating anything

Knip is the current tool for this. The older ones, depcheck and ts-prune, were both archived, Knip replaced both by doing dependency, export, and unused-file analysis together.

Run it once, with no setup, to see your project’s actual state:

npx knip

It’ll list unused files, unused exports, and unused dependencies. Read through it before you automate anything, some flagged items are false positives, entry points Knip doesn’t know are entry points, and those need a config file to exclude. Don’t wire a hook to a tool you haven’t looked at first.


Step 2

Add a config if you need one

If Knip flagged something that’s actually used, like a file only referenced by your build tooling, tell it so. Create knip.json in the project root:

{
  "entry": ["src/index.ts"],
  "project": ["src/**/*.ts"]
}

Adjust the paths to match your project. Re-run npx knip and confirm the false positives are gone.


Step 3

Install Husky

Husky manages git hooks so they live in the repo and travel with it, instead of sitting only on your machine.

npm install --save-dev husky
npx husky init

That creates a .husky/ folder and adds a prepare script to package.json, so the hook installs itself for anyone who clones the repo and runs npm install.


Step 4

Wire Knip to the push

husky init creates a sample .husky/pre-commit. For this, use pre-push instead, since a full dead-code scan on every single commit is slower than most people want mid-flow, but the moment before code leaves your machine is exactly the right time to check.

Create .husky/pre-push:

npx knip

That’s the whole hook. Knip exits with a non-zero status when it finds issues, and a non-zero exit from a git hook stops the git operation. No extra plumbing.


Step 5

Watch it actually block something

Prove it before you trust it. Add an obviously unused export somewhere in your source, an unimported function, a file nothing links to. Commit it, then push:

git add .
git commit -m "test: add unused export"
git push

The push should fail, with Knip’s output telling you exactly what it found. Delete the test code, push again, it goes through clean.


What this doesn’t cover

It’s a speed bump, not a wall. git push --no-verify skips every hook. That’s intentional, in an emergency you shouldn’t be blocked by a linter, but it means this catches accidents, not someone actively working around it.

It only runs on your machine. If more than one person works on the repo, each person needs to npm install for the hook to install locally. A CI check running the same npx knip command is the version nobody can skip.

Existing debt doesn’t get fixed by this. The hook stops new dead code from arriving. Cleaning out what’s already there is a separate, one-time pass.


The short version

  1. npx knip once, read the output, add a knip.json for false positives
  2. npm install --save-dev husky && npx husky init
  3. .husky/pre-push contains one line: npx knip
  4. Push something with an unused export, watch it get blocked
  5. --no-verify bypasses it on purpose; a CI check is the version that can’t be skipped

Sources

Written August 2026.

Prints to PDF from your browser — colours and all.