Pre-Commit Hooks That Call an LLM: Worth It or Not?
Guides

Pre-Commit Hooks That Call an LLM: Worth It or Not?

How to wire a model into a git pre-commit hook, which checks actually belong there, and the latency and cost budget that decides whether the team keeps it.

A pre-commit hook runs on the developer's machine, in the two seconds between typing the commit message and getting the prompt back. That is the entire design constraint. Anything you put there competes with muscle memory, and a hook that takes eight seconds gets bypassed with --no-verify within a week.

Which means the interesting question is not how to call a model from a hook. It is which checks are worth the latency, and which belong in CI where nobody is waiting.

The hook itself is trivial

A pre-commit hook is an executable file at .git/hooks/pre-commit. A non-zero exit blocks the commit. Everything else is your problem, including the fact that .git/hooks is not version controlled, so a hook you write locally does not reach anyone else.

Use the pre-commit framework or set core.hooksPath to a committed directory if you want the hook to be shared. Otherwise you have built a personal tool, which is fine, but do not expect team-wide behaviour from it.

The staged content is what matters, not the working tree. Read it with git diff --cached, because a developer who staged half a file expects the hook to see the staged half. Getting this wrong produces checks that pass on code that is not what gets committed.

#!/usr/bin/env bash
set -euo pipefail

DIFF=$(git diff --cached --unified=0 -- '*.ts' '*.py')
[ -z "$DIFF" ] && exit 0

# Hard cap: bail out rather than spend on a huge commit.
if [ "$(printf %s "$DIFF" | wc -c)" -gt 20000 ]; then
  echo "diff too large for pre-commit review, skipping" >&2
  exit 0
fi

BODY=$(jq -n --rawfile d /dev/stdin '{
  model: "qwen-3.6",
  max_tokens: 300,
  messages: [
    {role: "system", content: "Report only clear bugs. Reply OK if none."},
    {role: "user", content: $d}
  ]
}' <<< "$DIFF")

OUT=$(curl -sS --fail-with-body --max-time 8 \
  -H "Authorization: Bearer $LLM_API_KEY" \
  -H "Content-Type: application/json" \
  -d "$BODY" "$LLM_BASE_URL/chat/completions" \
  | jq -r '.choices[0].message.content')

[ "$OUT" = "OK" ] && exit 0
echo "$OUT" >&2
exit 1

Fail open, not closed

The most important line in that script is the timeout. A developer on a train with flaky tethering must still be able to commit. If the model is unreachable, the hook should let the commit through and print a warning, not sit there for thirty seconds and then block.

The same goes for a missing key. Someone who has just cloned the repo and has not set LLM_API_KEY should get a one-line notice, not a wall of curl errors and a commit they cannot make. Check for the variable at the top and exit zero if it is absent.

Blocking a commit is a strong action and models are not reliable enough to earn it unconditionally. Reserve the non-zero exit for checks with a near-zero false positive rate, and print everything else as advisory output.

Which checks actually belong here

Good pre-commit candidates share three properties: they are fast, they are local to the diff, and a human can verify the finding in seconds.

Secret detection is the strongest case, though a regex-based scanner catches most of it faster and for free — use a model as a second pass on what the regex flagged as ambiguous, not as the primary detector. Commit message quality is another good one, and cheap, since the input is two hundred characters rather than a diff. If that is the only thing you want, generating commit messages from the staged diff is a better-shaped problem than reviewing the code.

Bad candidates: architectural review, test coverage judgement, anything requiring the rest of the repository for context. Those need a large context window, several seconds, and often multiple turns. They belong in a CI pipeline or a review bot on the pull request, where latency costs nothing and the model can read more than the diff.

Latency is the whole budget

Set a target and enforce it. Under two seconds and people barely notice. Between two and five they notice but tolerate it. Past five seconds the hook is on borrowed time.

Three things dominate that number: prompt size, output length, and model choice. Cap the output hard — three hundred tokens is plenty for a list of findings, and generation time scales with output length far more than with input length. Then pick a model tuned for latency rather than capability; this is exactly the case where a cheap model is enough, because the task is pattern matching on a small diff, not reasoning about a system.

Time to first token is not the metric here, because nothing is streamed to a human who reads incrementally. You need the complete response before you can decide the exit code, so total round trip is what you budget against. The mechanics behind that number are covered in how inference latency actually breaks down.

Cost adds up quietly

A developer commits ten to thirty times a day. Ten engineers on that pattern is a few thousand requests a week, every one of them containing a diff. It is not a huge bill, but it is a continuous one, and unlike a CI run nobody sees it happen.

Cap the diff size and skip entirely above the threshold, as in the script above. Large commits are exactly the ones where a diff-only review is least useful anyway, because the change spans files the model cannot see. Filter to source file extensions so a lockfile update or a generated migration does not get sent at all.

Consider running the hook on pre-push instead. Pushes are perhaps a tenth as frequent as commits, the latency budget is more generous because a push already takes a second, and you still catch the problem before it reaches anyone else. For most teams that is the better trade.

Make it easy to bypass, and watch the bypasses

Everyone knows about --no-verify. Pretending otherwise wastes your time. Accept that the hook is advisory and design for it: if people bypass it constantly, the hook is wrong, not the people.

The honest way to run this is to treat the first month as an evaluation. Log what the hook flagged and whether the developer acted on it. If the acted-on rate is low, either the prompt needs work or the check does not belong in a hook. Deleting a hook that nobody benefits from is a legitimate outcome.

If you keep it, keep the prompt in a committed file rather than inline in the script, so changing the check is a reviewable diff rather than a shell edit. The same discipline described in prompt engineering for coding agents applies: be specific about what to report, and explicitly tell the model to stay silent when there is nothing to say.

A decision rule

Put a check in a pre-commit hook only if it is under two seconds, needs nothing beyond the staged diff, and has a false positive rate low enough that you would be comfortable blocking on it. If any of those three fail, move it to pre-push or to CI. Almost every interesting LLM check fails at least one of them, which is why the useful hooks tend to be small.

Common questions

Should a pre-commit hook block the commit when the model reports an issue?

Only for checks with a very low false positive rate, such as an obvious committed secret. For everything else print the finding as advisory output and exit zero, because a model confidently wrong once a day trains people to use --no-verify.

What happens if the developer is offline?

Fail open. Set a short curl timeout, exit zero when the call fails or the API key is missing, and print a one-line warning. A hook that blocks commits during an upstream outage gets deleted, not fixed.

Is pre-push better than pre-commit for this?

Usually. Pushes are far less frequent, the latency budget is larger because a push is already slow, and the problem is still caught before anyone else sees the code. The cost drops by roughly an order of magnitude too.

Similar articles

Building a Changelog Generator People Actually Read
Guides
Guides·9 min read

Building a Changelog Generator People Actually Read

Restating commit subjects is not a changelog. How to pick the right input, separate classification from writing, handle reverts, and keep regeneration deterministic.

Read
Building a PR Summariser Reviewers Do Not Skip
Guides
Guides·9 min read

Building a PR Summariser Reviewers Do Not Skip

Most PR summary bots restate the diff and get ignored within a fortnight. What reviewers actually need, how to select the diff, and how to keep cost per PR predictable.

Read
GitHub Actions LLM Setup: A Workflow That Reviews PRs
Guides
Guides·9 min read

GitHub Actions LLM Setup: A Workflow That Reviews PRs

A working GitHub Actions job that calls an OpenAI-compatible model on pull requests — repository secrets, timeouts, rate limits, PR comments and cost control.

Read