AI-Powered Git Commit Messages: Useful or Just Annoying
Letting a model describe your diff, and whether you should

Contents
There’s a particular flavour of laziness that git commit messages bring out in people. You’ve just spent an hour on a fiddly change, the work is done, and now a text editor opens demanding you explain yourself. So you type “fix stuff” and move on, and three months later you’re spelunking through git log cursing your past self. We’ve all done it, and we’ve all been on the receiving end of someone else’s — the commit that touches forty files and explains itself with a single shrug of a word. The promise of offloading that drudgery to a model is genuinely appealing precisely because the task is one humans reliably do badly when tired. The pitch for AI commit messages is simple: feed the staged diff to a model, get back a tidy conventional-commit summary, accept it, done. I’ve been running this on my own repos for a while. It’s genuinely useful and quietly dangerous, and which one depends entirely on how you wire it up.
How it actually works
The mechanism is unglamorous and that’s the good news — there’s no magic. A small script grabs the staged diff, wraps it in a prompt, sends it to a model, and drops the result where git expects it. The cleanest place to hook in is prepare-commit-msg, a git hook that runs before your editor opens and can pre-fill the message:
| |
Because it runs as prepare-commit-msg, the suggestion lands in your editor as a draft. You still see it, you still get to fix it, you still hit save. That “human in the loop” detail is the whole game — it’s the difference between an assistant and an automaton.
If hand-rolling the hook feels like too much, that’s exactly the job a small purpose-built CLI does well — aicommits, opencommit, and friends all wrap this same pattern. I tend to prefer the fifteen lines above because I can see exactly what they do and point them at a local model, which is the sort of “build the small sharp tool yourself” instinct I wrote about in writing CLI tools in Go. Either way the mechanism is identical: diff in, summary out, draft in your editor.
A note on what git diff --cached actually feeds the model
The quality of the suggestion is bounded by what you hand the model, and git diff --cached is a surprisingly leaky abstraction. It shows the patch hunks plus a few lines of context, which is great for a focused change and useless for one where the meaning lives in a file the diff doesn’t touch. Rename a function and the diff is crisp; change a constant whose significance is three modules away and the model sees a one-line edit with no idea why it matters. Understanding what git is really storing under the hood — that a commit is a snapshot, and the diff is a derived view computed between two trees — makes the limitation obvious; I unpack that machinery in git internals. The model isn’t reading your repository’s history or your issue tracker. It’s reading one synthetic patch, and it will confidently narrate exactly that and nothing more.
For very large diffs you also hit a practical wall: dump 4,000 lines of diff into an 8B model’s context and the summary degrades into vague mush. The pragmatic fix is to cap it — feed git diff --cached --stat plus the first N lines of the real patch, or summarise file-by-file — but honestly, a diff that big is the tool telling you the commit should have been three commits.
Where it earns its keep
It’s at its best on the commits you’d otherwise phone in. A focused diff — rename a function, tweak a config, add a guard clause — produces a description that’s accurate and better-formatted than what I’d have bothered to write. It’s quietly good at the boring discipline I’m bad at: getting the conventional-commit prefix right, keeping the subject under 72 characters, summarising a multi-file change into coherent bullets. For a homelab repo where I’m the only reader, that consistency makes git log skimmable in a way my hand-typed messages never were.
Running the model locally via Ollama matters here for a reason beyond cost: your diff is your source code. Piping every staged change to a hosted API is a data-exfiltration decision, and for work repositories often a policy violation. A 7B–8B local model is more than capable of summarising a diff, so there’s no good reason to send it off-machine.
Where it’s actively misleading
Here’s the catch, and it’s the important half of the post. The diff shows what changed, not why. A model can see that you swapped > for >= and will faithfully report “adjusted boundary condition.” What it cannot know is that this fixes an off-by-one that corrupted invoices on month boundaries, reported by a customer, costing a day to track down. That why is the entire value of a commit message, and it’s exactly the part the AI can’t supply.
So the failure mode isn’t gibberish — it’s worse. It’s plausible, well-formatted prose that describes the mechanics convincingly enough that you stop thinking and accept it. You end up with a log that reads beautifully and tells you nothing when you actually need it during a 2am bisect. A bad “fix stuff” at least signals “I didn’t bother.” A confident AI summary of the what can lull you into not writing the why.
There’s a smaller annoyance too: on a big mixed diff the model picks one theme and underplays the rest, which is really the model telling you the commit should have been split.
Tuning the prompt actually matters
Most of the quality difference between “useful” and “annoying” lives in the prompt, not the model. A few changes that paid off for me:
- Pin the format hard. “One subject line under 72 chars, imperative mood, Conventional Commits prefix, then a blank line, then 1–3 bullets, no prose paragraphs.” Models left to their own devices write chatty multi-sentence descriptions that nobody reads.
- Forbid invention explicitly. Add “Describe only what the diff shows. Do not speculate about intent or impact.” It won’t fully stop the model inventing a rationale, but it measurably reduces the confident fiction.
- Give it the branch name. Piping in
$(git branch --show-current)lets the model pick upfeat/-style context the diff alone doesn’t carry, which nudges the prefix right more often.
You can layer this into the same hook by extending the prompt string; nothing about the plumbing changes. And because the hook is just a script that runs at a known point in git’s lifecycle, it composes naturally with the rest of your local guardrails — it’s the commit-message sibling of pre-commit hooks catching mistakes before they reach the repo. One fires on staged content to format and describe; the other fires to lint and block. Same lifecycle, different job.
How I’d actually use it
The setup that works for me draws a clear line. Use the AI draft as a starting point that you must edit, never an auto-commit. The prepare-commit-msg approach above does exactly this — it pre-fills and gets out of the way. Treat the generated subject and bullets as a free first pass at the what, then spend the ten seconds the tool just saved you adding the why in your own words. If the diff is so mixed the model flails, take that as a hint to git add -p and split it.
What I’d avoid: any tool that runs on commit and pushes without showing you the message, anything that sends diffs to a cloud API by default, and the temptation to trust the draft blindly because it reads well.
The team dimension, and why it’s different
Everything above is calibrated for a solo repo where I’m the only reader and the only cost of a weak message is future-me’s mild annoyance. On a team the calculus shifts, and not always in the obvious direction.
The upside scales: consistent Conventional Commits across a dozen contributors makes automated changelog generation and semantic-version bumping actually work, instead of breaking the moment someone writes “misc fixes.” If your release tooling parses commit prefixes, a tool that reliably produces feat:/fix:/chore: is genuinely valuable, because humans are inconsistent about it and machines aren’t.
But the downside scales faster, and in two specific ways. First, the data-leak risk stops being theoretical: one teammate wiring the hook to a hosted API by default means proprietary diffs leaving the building, possibly in breach of policy nobody re-reads. The mitigation is organisational, not technical — agree, in writing, that diffs go to a local model or not at all, before anyone gets clever. Second, a team that accepts AI drafts uncritically degrades its collective history, and a shared git log full of confident descriptions of the what with none of the why is worse than a sparse one, because it looks complete while being useless during an incident. The 2am bisect that needs the real reason behind a change is exactly when this bites, and on a team it’s rarely the author doing the bisecting.
So if you roll this out for a team, roll out the policy with it: local model only, draft-not-auto, and an explicit norm that the human adds the why. The tooling is fifteen lines; the agreement is the hard part.
Useful or annoying?
Both, and the verdict depends on the discipline you bring. Wired as a local, editable, pre-fill draft, it’s a small genuine quality-of-life win — better formatting, less friction, a nudge toward smaller commits. Wired as a hands-off auto-committer talking to a cloud model, it’s an engine for confidently meaningless history and a quiet data leak. The tooling is trivial; the judgement isn’t. Keep the human in the loop, keep the model on your machine, and use it to handle the what so you can concentrate on the why. For a solo tinkerer, that’s worth the fifteen lines of hook. For a team, agree the policy before someone wires it to a hosted API. Used that way, it’s one of the few “AI for developers” tools whose value is real and whose downside is entirely within your control — which, given how much of that category is hype, is faint but genuine praise.




