Why I’m Rethinking Pre-Commit Hooks

This post isn’t really about tooling. It’s about flow-state, feedback loops, and how easy it is to optimise for the right things in the wrong order.

For a long time, I deliberately enforced heavy pre-commit hooks. I was optimising for fast feedback, early signals, and fewer surprises later in the pipeline. At the time, that felt like a clear DevEx win.

That decision wasn’t accidental or careless. It came from a genuine desire to improve developer experience. I cared about the craft, consistency, and catching problems early. Pre-commit hooks felt like the right tool to support that.

And for a while, they did.

Where it started to break down

On one large, long-running project, we leaned heavily into this approach.

Pre-commit hooks didn’t just format files or catch obvious mistakes. They ran linters, built Storybook, and executed test suites. Everything had to pass before engineers could commit.

Nothing was failing. The checks were doing precisely what we asked of them.

The problem was time.

Each commit meant waiting. Thirty or forty seconds, sometimes more. Not because something was broken, but because the system was busy proving correctness. You’d finish a thought, commit your work, and then stop. Staring at a spinner and feeling context slowly drain away.

On days when I was making lots of small, iterative commits, that pause happened again and again. Not long enough to justify context switching, but long enough to interrupt thinking. Over time, that interruption became the dominant cost.

What struck me wasn’t that the checks were wrong. It was that they were happening at the worst possible moment.

In hindsight, this wasn’t a mistake so much as an incomplete optimisation.

The responsibility of defaults

Around the same time, I’d also helped bake this approach into our defaults.

Not just on one project, but in how we scaffold new work more broadly. Pre-commit enforcement became part of wp-scaffold, with linters and code style checks running automatically across CSS, JavaScript, and PHP.

That meant the cost wasn’t theoretical. It showed up immediately, and repeatedly, right at the moment engineers were trying to commit work and move on with a thought.

Those defaults influenced how people started projects, how they iterated, and where friction appeared day to day, often before teams had any real opportunity to question the trade-offs.

That was when the weight of the decision really landed.

This wasn’t just about one team or one codebase anymore. It was about what we were optimising for by default, and who we were asking to absorb the cost of that optimisation.

A fair counterpoint

This isn’t an argument that pre-commit hooks are bad.

If your hooks are genuinely fast, almost invisible, and maybe auto-fixing trivial issues, they can absolutely help. In small codebases, or when the checks are cheap, that trade-off can still make sense.

The problems start when too much responsibility gets piled into that moment.

Slow or blocking pre-commit hooks don’t feel like safety nets anymore. They feel like interruptions. And at scale, those interruptions add up quickly.

The issue isn’t the checks themselves, it’s how and when they’re enforced.

Where I’ve landed since

On every project I’ve worked on since, I haven’t set up any heavy pre-commit enforcement.

That doesn’t mean standards slipped. Linters still run. Tests still run. Static analysis still runs. Formatting still happens. The difference is when and how they’re enforced.

Roughly speaking:

  • Engineers can commit work while they’re still thinking, rather than being stopped on every save-and-commit cycle.
  • Local tooling is something you opt into when you’re ready to tidy things up, not something that blocks you from working in the way you prefer.
  • CI is where enforcement really happens, before a pull request is merged. 

Personally, the last thing I do before pushing a branch is run the full suite. I want that pause to be intentional. I want to decide when I stop and check my work, rather than having that decision made for me.

That distinction matters.

This has become even more important with AI-assisted workflows. AI is very good at helping you get logic onto the page quickly, but it often takes a few passes to align with a project’s style and conventions. Enforcing strict checks too early multiplies friction at precisely the point where iteration should be cheap.

Coming back with fresh eyes

What really prompted me to write this now wasn’t the original realisation at the time.

I actually stepped away from that project for about a year. In the meantime, I worked on other codebases where I deliberately didn’t use pre-commit enforcement at all. CI handled the guarantees. Local tooling stayed out of the way.

Recently, I dipped back into the original project to help out with some work.

And the pre-commit hooks hit me hard.

The same pauses. The same waiting. The same feeling of being pulled out of a train of thought, just as I was lining up what to tackle next. Only this time, it stood out immediately.

Coming back to it after working on other projects, what had once felt normal now felt actively unhelpful. That contrast was the real confirmation for me.

It wasn’t that the checks were wrong. It was that, after experiencing uninterrupted flow-state for a while, the cost of breaking it was suddenly very obvious.

The real lesson

Looking back, the core issue wasn’t tooling at all.

I’d optimised heavily for fast feedback, without really accounting for how expensive it can be to recover once flow-state is broken. Feedback that arrives a little later is usually fine. Being pulled out of a thought over and over again isn’t.

These days, when I’m deciding where to enforce something, I try to double-check it in a simple way. If a check takes long enough to interrupt my train of thought, I start questioning whether it belongs there at all.

For me, that reframed how I think about pre-commit hooks.

Husky wasn’t really the problem.
It was all the work we asked our pre-commit hooks to do.