Skip to content

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

A terminal printout taped to graph paper: a commit blocked by a local model reviewer, exit code 1 ringed in red pencil.

Bench log · AI Tools · 9 min read

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.

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.

curl http://127.0.0.1:11434/api/generate -d '{"model": "qwen2.5-coder:7b", "keep_alive": "30m"}'

Kept: keep_alive: "30m"; preload optional.

Entry 6: the same diff should get the same answer

Entry 6 · options

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]

Kept: language: unsupported, pass_filenames: false.

Trial runs: four commits, four exit codes

12 Nov 2026 · dash, curl 8.5.0, Python 3.12.3

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.

RunConditionWhat the hook printedgit commit exit
1No server listeningllm-review: no answer from http://127.0.0.1:18434, commit allowed0
2Server returns blockllm-review: app.js:2 console.log left in1
3Server returns pass(nothing)0
441,174-byte diffllm-review: staged diff is 41174 bytes, over 20000, skipped0

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.

The knobs, in one table

SettingOllama defaultUsed hereWhy
streamstreaming onfalseOne JSON object to parse
formatnoneJSON schemaA verdict the script can read
num_ctx4096 tokens8192Room for a real diff
keep_alive5m30mFewer cold loads between commits
temperature, seedmodel's own0, 7Same diff, same verdict
Diff capnone20,000 bytesSkip what will not fit, and say so

Sources, all opened on 12 November 2026: git githooks manual, Ollama API reference, Ollama FAQ, pre-commit documentation, Ollama library: qwen2.5-coder.

Built a tool that sits in a developer's workflow like this? List it on CodeTrendy so the people looking for one can find it: submit your tool.