We use analytics cookies to see how the site is used. They are only set if you accept. Accepting also turns on session recording (pages viewed, clicks and scrolls) so we can fix problems; what you type stays hidden. It also lets Meta (Facebook) see your visit, so we can measure our ads. Read the privacy policy
Ollama Pre-Commit Hook: Field Notes on Wiring a Local Model Into git commit
A bench log of putting a local Ollama model in front of every commit: where the hook sits, how to get JSON back, the 4096-token default, cold starts, and why the hook must let the commit through when the model is down.
By CodeTrendy Editorial TeamPublished Reviewed
$ git commit -m "wip"
llm-review: app.js:2 console.log left in
$ echo $?
1
blocked by the local model, as hoped
These notes put a local language model, served by Ollama, between git add and the commit, and log each decision with the document it rests on. The hook printed at the end was run on 12 November 2026 against a small stand-in server that answers in the shape of Ollama's API, so every exit code below is a real one; the quality of a real model's review was not measured here, and the entries say where that matters.
Entry 1: where the reviewer can stand
Entry 1 · git
Git runs .git/hooks/pre-commit before it asks for the commit message. The githooks manual gives the two facts that shape everything else: the hook takes no parameters, and a non-zero exit aborts the commit. It also says the hook "can be bypassed with the --no-verify option".
So the hook reads the staged change itself (git diff --cached), and it can only ever be a guard rail, never a gate: anyone can step around it. That is the right size for a model's opinion anyway.
Kept: read the staged diff, exit 1 only to block.
Entry 2: one request, one answer
Entry 2 · Ollama API
Ollama listens on 127.0.0.1 port 11434 by default (Ollama FAQ), so the diff never leaves the machine. The API reference describes POST /api/generate as a streaming endpoint; with "stream": false it returns a single JSON object with the whole reply in response. A hook has no use for tokens arriving one by one, so streaming goes off.
Kept: /api/generate, stream: false.
Entry 3: making the verdict machine-readable
Entry 3 · structured output
Free text is useless to a shell script. The same API reference says the format field takes either json or a JSON schema, and that the model then returns a response matching the schema. It also warns to ask for JSON in the prompt as well, or the model "may generate large amounts whitespace".
The schema here has two fields: verdict, limited to pass or block, and a list of findings with file, line and issue. The script reads verdict and nothing else decides the exit code.
Kept: a schema in format, and "Answer in JSON" in the prompt.
Entry 4: the context window is smaller than the diff
Entry 4 · num_ctx
The FAQ states that Ollama uses a context window of 4096 tokens by default, changed per request with options.num_ctx or for the server with OLLAMA_CONTEXT_LENGTH. The prompt, the whole diff and the answer have to fit in it. A refactor that touches twenty files does not.
The hook asks for num_ctx: 8192 and skips any staged diff over 20,000 bytes, printing that it skipped. The byte cap is a rough guard picked for this log, not a token count: tune it to your model and your memory. Lock files are left out of the diff, since no reviewer reads them.
Kept: raise num_ctx, cap the diff, say so when skipping.
Entry 5: the first commit of the day is the slow one
Entry 5 · keep_alive
A model has to be loaded into memory before it answers. The API reference gives keep_alive a default of 5m: five idle minutes and the model is unloaded, so the next commit pays the load again. The final response reports this cost as load_duration, in nanoseconds, so you can see it on your own machine.
The hook sends keep_alive: "30m". The FAQ also shows how to preload a model with an empty request, which fits in a shell profile or an editor start-up task.
A reviewer that blocks a commit, then passes the identical retry, teaches everyone to retry. The API reference passes temperature through options and says that for reproducible outputs you set seed to a number. The hook sets temperature: 0 and a fixed seed.
Kept: temperature 0, fixed seed.
Entry 7: when the model is down, commit anyway
Entry 7 · fail open
The laptop is on battery, Ollama is not running, or the model is not pulled yet. If the hook fails closed, every one of those becomes a blocked commit and a --no-verify habit. So any failure to get an answer (no connection, an HTTP error, a 90-second timeout, a reply that is not JSON) prints one line and exits 0. Only a well-formed block verdict stops the commit. The trial runs below show both paths.
Kept: fail open, loudly.
Entry 8: a short list of what may block
Entry 8 · prompt
A model asked to "review this code" will always find something: a name, a missing comment, a style it prefers. If any finding blocks, the hook blocks every commit. The prompt names the only reasons to block (a leaked secret or key, a debug statement left in, an obvious crash such as a null dereference, code that cannot run) and says style and naming never block. Findings still print on a pass; they just do not stop anything.
How well a given model keeps to that list is the part this log could not test, and it is the part to watch in your first week: count the blocks you overrode.
Kept: a closed list of block reasons; everything else is advice.
Entry 9: which model to pull
Entry 9 · model
The hook reads its model from LLM_REVIEW_MODEL and defaults to qwen2.5-coder:7b. The Ollama library page lists that family in six sizes, from 0.5B to 32B parameters, so the same hook can run a small model on a laptop and a large one on a desktop with a big GPU. ollama ps shows whether a loaded model sits on the GPU, the CPU or both (FAQ).
ollama pull qwen2.5-coder:7b
Kept: model in an environment variable, 7B as the default.
The hook, printed in full
Printout · .githooks/llm-review
POSIX sh, curl and python3 for the JSON. Save it, make it executable, and copy it to .git/hooks/pre-commit or call it from the pre-commit framework below.
#!/bin/sh
# Pre-commit reviewer: sends the staged diff to a local Ollama model.
# Blocks the commit only on a "block" verdict. Fails open if Ollama is down.
MODEL="${LLM_REVIEW_MODEL:-qwen2.5-coder:7b}"
HOST="${OLLAMA_HOST_URL:-http://127.0.0.1:11434}"
MAX_BYTES="${LLM_REVIEW_MAX_BYTES:-20000}"
diff=$(git diff --cached --no-color --unified=3 -- . ':(exclude)*.lock' ':(exclude)package-lock.json')
[ -z "$diff" ] && exit 0
size=$(printf '%s' "$diff" | wc -c)
if [ "$size" -gt "$MAX_BYTES" ]; then
echo "llm-review: staged diff is $size bytes, over $MAX_BYTES, skipped" >&2
exit 0
fi
payload=$(printf '%s' "$diff" | MODEL="$MODEL" python3 -c '
import json, os, sys
diff = sys.stdin.read()
schema = {"type": "object", "required": ["verdict", "findings"], "properties": {
"verdict": {"type": "string", "enum": ["pass", "block"]},
"findings": {"type": "array", "items": {"type": "object",
"required": ["file", "line", "issue"], "properties": {
"file": {"type": "string"}, "line": {"type": "integer"}, "issue": {"type": "string"}}}}}}
prompt = ("You review a staged git diff before commit. Answer in JSON. "
"Use verdict block only for: a leaked secret or key, a debug statement left in, "
"an obvious crash such as a null dereference, or code that cannot run. "
"Style, naming and taste are never a block. Diff:\n\n" + diff)
print(json.dumps({"model": os.environ["MODEL"], "prompt": prompt, "stream": False,
"format": schema, "keep_alive": "30m",
"options": {"temperature": 0, "seed": 7, "num_ctx": 8192}}))
')
reply=$(printf '%s' "$payload" | curl -sS --fail -m 90 "$HOST/api/generate" -H 'Content-Type: application/json' -d @-) || {
echo "llm-review: no answer from $HOST, commit allowed" >&2
exit 0
}
printf '%s' "$reply" | python3 -c '
import json, sys
outer = json.load(sys.stdin)
try:
r = json.loads(outer.get("response", ""))
except ValueError:
print("llm-review: reply was not JSON, commit allowed", file=sys.stderr); sys.exit(0)
for f in r.get("findings", []):
print("llm-review: %s:%s %s" % (f.get("file"), f.get("line"), f.get("issue")), file=sys.stderr)
sys.exit(1 if r.get("verdict") == "block" else 0)
'
Wiring it through the pre-commit framework
Entry 10 · pre-commit
Teams that already use pre-commit can register the script as a local hook. Its docs list the parts that matter here: pass_filenames defaults to true and must be false, because the script reads the diff itself; the language for a plain system executable is now called unsupported ("new in 4.4.0: previously language: system", and the old alias will be removed); and SKIP=llm-review git commit skips this one hook without turning the others off.
repos:
- repo: local
hooks:
- id: llm-review
name: local LLM review of the staged diff
entry: .githooks/llm-review
language: unsupported
pass_filenames: false
stages: [pre-commit]
The hook was installed in a scratch repository and pointed at a stand-in server on a spare port that answers /api/generate with a fixed verdict. That tests the plumbing (diff, request, parsing, exit codes), not a model's judgement.
Run
Condition
What the hook printed
git commit exit
1
No server listening
llm-review: no answer from http://127.0.0.1:18434, commit allowed
0
2
Server returns block
llm-review: app.js:2 console.log left in
1
3
Server returns pass
(nothing)
0
4
41,174-byte diff
llm-review: staged diff is 41174 bytes, over 20000, skipped
0
In run 2 the request the server received carried stream: false, the schema with verdict limited to pass and block, and the options temperature 0, seed 7 and num_ctx 8192.