Your commit message, checked against what you actually changed.
git-crux reads the staged diff, judges the message you wrote against it, and — when the message is vague, incomplete, or plain wrong — suggests a sharper one. It sharpens your stated intent rather than generating from scratch, so the message still says what you meant.
Run it as git crux -m "...", or install the hook and let plain git commit
handle itself. It never blocks a commit: if the model is unreachable or
confused, your commit goes through untouched.
- Conventional Commits by default —
feat:,fix:,chore:, … inferred from the diff;GIT_CRUX_STYLE=plainfor a plain imperative subject instead - Any OpenAI-compatible endpoint — OpenAI's
gpt-4o-miniout of the box, or keep diffs on your machine with LM Studio, Ollama (:11434/v1), or llama.cpp viaGIT_CRUX_BASE_URL - Handles big diffs — larger changes are split, summarized, then judged as a whole, so no file is silently dropped
- Measured, not vibes — a labelled evaluation set scores the prompt, so a change to it can be checked instead of eyeballed
Note: with the OpenAI default, each reviewed diff is sent to OpenAI.
It gets to the crux of the change: does your message actually describe what you did?
With Homebrew:
brew install jonhadfield/tap/git-cruxOr, on macOS, with the signed installer:
curl -fLO https://github.com/jonhadfield/git-crux/releases/latest/download/git-crux_macos.pkg
sudo installer -pkg git-crux_macos.pkg -target /That puts git-crux in /usr/local/bin. The package is notarized with a
stapled ticket, so it installs with no Gatekeeper warning; a bare binary from
one of the tarballs is quarantined by the browser and refused instead.
Double-clicking the .pkg works too.
Or with Go 1.22 or later:
go install github.com/jonhadfield/git-crux@latestOr from a checkout:
go build -o git-crux .Because git resolves git crux to a git-crux binary on PATH, you get the
git crux subcommand for free.
Then point it at a model. With OPENAI_API_KEY set you are already done. To
keep diffs on your machine instead, start a local server — with LM Studio, run
its server (default port 1234) and load a model — and set GIT_CRUX_BASE_URL
(see Configuration). Model choice matters a lot (see Status): a 14B-class
instruct model such as microsoft/phi-4 is the realistic local minimum.
As an explicit command (no setup beyond install) — the screenshot above shows a full run:
git add .
git crux -m "updates my application"As a hook, so plain git commit is handled automatically:
git crux init # installs .git/hooks/prepare-commit-msg
git commit -m "fix" # git-crux steps in only if the message is off-point
git commit # no -m: git-crux pre-fills the editor with a generated messageTo cover every repository at once, install globally via git's core.hooksPath:
git crux init --global # installs into ~/.config/git/hooks and points core.hooksPath thereNote: core.hooksPath replaces per-repo .git/hooks, so any existing repo-local
hooks won't run unless moved into the shared directory.
With no -m, the generated message lands in your $EDITOR — edit and save to
accept, or empty the buffer to abort. (Merge, squash, and amend commits are left
untouched.)
Dry run — print the verdict as JSON without committing (handy for testing):
git crux -m "fix stuff" -dry-runAll optional; the defaults target OpenAI's gpt-4o-mini. If no API key is
found (neither GIT_CRUX_API_KEY nor OPENAI_API_KEY), git-crux falls back to
a local server (http://localhost:1234/v1, model microsoft/phi-4) instead of
erroring — so it works out of the box whether or not you have a key.
| Env var | Default | Meaning |
|---|---|---|
GIT_CRUX_BASE_URL |
https://api.openai.com/v1 |
OpenAI-compatible base URL. Set to e.g. http://localhost:1234/v1 to run local. |
GIT_CRUX_MODEL |
gpt-4o-mini |
Model id to request. |
GIT_CRUX_API_KEY |
(falls back to OPENAI_API_KEY) |
Bearer token for hosted APIs; ignored by local servers. |
GIT_CRUX_SKIP |
(unset) | Set to any value to disable evaluation. |
GIT_CRUX_STYLE |
conventional |
Message style: conventional (Conventional Commits) or plain (imperative subject). |
GIT_CRUX_CONTEXT |
(detected, else from model) | Model context window in tokens; sizes the diff sent for review. Rarely needed — see Context detection. |
GIT_CRUX_MAX_DIFF |
(auto from context) | Hard cap on diff bytes sent; overrides the context-derived budget. |
GIT_CRUX_REASONING_EFFORT |
(unset) | Sent as reasoning_effort when set; omitted entirely when not. See Reasoning models. |
A strict json_schema request and a model with a thinking phase can collide,
and the answer comes back useless in one of two ways, both seen against LM
Studio:
- nothing at all —
finish_reason: "stop"withcontent: ""(a 27B qwen3) - a schema-shaped shell —
{"verdict":"incomplete","suggestion":"","reason":""}withreasoning_tokens: 0, the grammar having squeezed out the thinking phase (deepseek-r1-distill-qwen-7b)
git-crux handles both on its own: it retries once without response_format and
extracts the JSON from the free-form reply, tolerating a <think> block,
markdown fences and prose either side. The verdict is then validated in code,
since the schema is no longer enforcing it.
No configuration is needed, and the retry is the better answer rather than just a rescue: on the same prompt, the constrained call spent 32 tokens producing empty strings while the unconstrained one spent 251 reasoning tokens and returned a grounded suggestion.
A model is routinely loaded with a smaller window than its family's maximum —
LM Studio will happily serve deepseek-r1-distill-qwen-7b at 8192 tokens when
the model supports 131072. Sizing the diff from the larger number means every
request overflows.
git-crux asks the server first. LM Studio reports the real figure through its
native /api/v0/models as loaded_context_length, which is queried once per
run and used when available; otherwise the window is inferred from the model id
as before. GIT_CRUX_CONTEXT still overrides both.
This matters because an overflow is not always reported as one. LM Studio
answers an oversized prompt with HTTP 200 and an empty choices array — no
error text, no usage — so git-crux treats that shape as an overflow and retries
with a smaller diff, rather than reporting a dead end.
GIT_CRUX_REASONING_EFFORT is the escape hatch if you would rather suppress the
thinking phase outright:
export GIT_CRUX_REASONING_EFFORT=noneThe accepted levels are the server's, not git-crux's, and they vary by model —
the value is passed through untouched. Set it only for a server that expects it:
models without a thinking phase reject the field outright, and OpenAI's
gpt-4o-mini answers 400 Unrecognized request argument supplied: reasoning_effort. That is why it is omitted from the request unless you set it.
git-crux uses Go's standard proxy handling, so the usual environment variables work with no extra configuration:
export HTTPS_PROXY=http://proxy.example.com:3128 # used for https:// endpoints
export HTTP_PROXY=http://proxy.example.com:3128 # used for http:// endpoints
export NO_PROXY=api.internal.example.com # comma-separated bypass listLowercase spellings (https_proxy, and so on) work too. localhost and
127.0.0.1 are always bypassed, so a local model server is reached directly
even with a proxy set.
| Flag | Default | Meaning |
|---|---|---|
-m |
— | Commit message. Omit to have git-crux generate one. |
-model |
(resolved at runtime) | Model to use. Defaults to GIT_CRUX_MODEL, else the active server profile (gpt-4o-mini for OpenAI, microsoft/phi-4 local). |
-style |
conventional |
Message style: conventional or plain (overrides GIT_CRUX_STYLE). |
-no-ai |
false |
Skip evaluation, commit as-is. |
-dry-run |
false |
Print the verdict JSON and exit. |
By default git-crux follows the
Conventional Commits standard: every
suggested subject begins with a type prefix the model infers from the diff —
feat: for a new capability, fix: for a correction, perf:, refactor:,
docs:, test:, build:, ci:, style:, chore:, or revert:. A breaking
change is marked with ! (e.g. feat!:). Under this style a message that
describes the change well but lacks a valid prefix is still flagged, so a
plain fix login bug is sharpened to fix: correct login validation.
Prefer terse, prefix-free subjects? Switch to the plain imperative style:
git crux -m "fix login" -style plain # one-off
export GIT_CRUX_STYLE=plain # for the session / in your shell rcType selection is left to the model from the diff; there's no flag to pin a type
(write the prefix yourself with -m if you want to override it).
- Fails open. If the model server is unreachable, errors, or returns unparseable output, the commit proceeds untouched. git-crux never blocks a commit.
- Quiet when the message is good. It only prompts on a
vague,incomplete, orwrongverdict. - Chunks large diffs. A diff that fits the model's budget is reviewed in one call. A larger one is split into parts, each summarized separately, then judged as a whole from those summaries — so no file is dropped. Very large diffs are capped at a fixed number of parts (the rest is covered by the file map).
- Skips non-interactive contexts. No terminal, or
CIset → does nothing. - Skips merge / squash / amend commits.
- Bypass any time with
GIT_CRUX_SKIP=1orgit commit --no-verify. - The hook chains to a pre-existing
prepare-commit-msghook (preserved asprepare-commit-msg.local).
v1.0 — works end to end against OpenAI and OpenAI-compatible local servers.
Follows Conventional Commits by default, installs globally via core.hooksPath,
chunks large diffs, shows a spinner while the model runs, and cancels cleanly on
Ctrl-C.
Model quality is the gating factor, especially for generating messages
(bare git crux / empty git commit), which needs a capable model with enough
context: gpt-4o-mini is reliable, while small local models (e.g. an 8K-context
microsoft/phi-4) tend to parrot the prompt or truncate large diffs. Verdicts
are requested at temperature 0 for determinism. The prompt is tuned against the
curated evaluation set (see above), not a broad benchmark, so expect rough edges
on unusual diffs.
Because the verdict is the product of a prompt, changing the prompt can quietly
regress it. A curated set of labelled cases lives in testdata/eval/ — each is a
commit message, a staged diff, and the expected verdict — so you can measure
before and after a prompt change instead of eyeballing it.
make eval # needs model env (OPENAI_API_KEY, or GIT_CRUX_BASE_URL)
GIT_CRUX_EVAL_MIN=0.8 make eval # also fail if verdict accuracy drops below 80%The runner prints a per-case PASS/FAIL table and a confusion matrix. The corpus
itself is validity-checked on every go test run (no model needed), so a
malformed or mislabelled case fails CI. Add a case by dropping a new directory
into testdata/eval/ with message.txt, diff.patch, and want.json.
The labels encode git-crux's intended behaviour; a model that disagrees on a borderline case is exactly the signal the set exists to surface.
make build # -> ./git-crux
make install # go install
make test # go test ./...MIT © 2026 Jon Hadfield
