Home Features For Vibe Coders For Developers Pricing Documentation Services Contact
← Back to blog

How to set up pre-commit hooks that actually catch bugs

Most pre-commit hooks I’ve seen just run a formatter and call it done. That’s not catching bugs. That’s enforcing style.

Here’s the setup I actually use, and why each piece earns its place.

The toolchain

I use Lefthook. Husky was the standard for years, but Lefthook is faster, runs commands in parallel, and doesn’t shell out through Node for every check. If you’re already on Husky and it works, fine. If you’re starting fresh, use Lefthook.

# lefthook.yml
pre-commit:
  parallel: true
  commands:
    typecheck:
      run: pnpm tsc --noEmit
    lint:
      run: pnpm eslint {staged_files}
      glob: "*.{ts,tsx,js,jsx}"
    format:
      run: pnpm prettier --check {staged_files}
      glob: "*.{ts,tsx,js,jsx,css,md}"
    test-changed:
      run: pnpm vitest related --run {staged_files}
      glob: "*.{ts,tsx}"

Notice what’s there and what isn’t.

What’s there

Typecheck on the whole project. TypeScript is fast at incremental compilation. If a function signature changed in utils.ts, every file that imports it could be broken, even files you didn’t touch. Running tsc on staged files alone misses this.

Lint on staged files only. ESLint on the whole project is slow and produces noise about pre-existing issues you don’t want to fix in this PR. Staged-files lint catches what you broke.

Format check, not auto-fix. Auto-fix is convenient until it silently rewrites a file you carefully crafted, your test breaks, and you lose 10 minutes figuring out why. Show me what’s wrong, let me fix it.

Tests related to the changed files. Vitest’s related flag computes the dependency graph and only runs tests whose code paths touch your staged files. It’s the difference between a 2-second hook and a 60-second hook on a large project.

What’s not there

No full test suite. Pre-commit isn’t the place. Run those in CI, where the wait doesn’t break your flow.

No security scanning. Pre-commit is too late. Run security scans on a watch loop or in CI. By the time you’re committing, the vuln is already in your branch.

No “no-console” or “no-debugger” hooks. ESLint already has rules for this. Don’t duplicate.

The escape hatch

git commit --no-verify skips the hooks. I leave this enabled, intentionally. There are days when you need to commit a WIP at 5pm and finish it tomorrow. Hooks that punish you for valid workflows will get bypassed, or worse, disabled.

What I’d add if I cared more

A check that fails if package.json changed but pnpm-lock.yaml didn’t, or vice versa. The number of times I’ve seen “Why is CI installing different versions?” because of a desynced lockfile is too high.

That one took me an hour the first time I debugged it. The hook to prevent it is three lines.

Join the feed

Get feature releases and engineering updates delivered to your inbox.