AI Prompts for Better Git Commit Messages
Most AI commit message tools just summarize the diff. Here's the AI prompt and git hook I use that actually captures the real reason behind a change.

I had a client project last month where I went back through three weeks of commits trying to figure out why a particular caching layer existed, and every single commit on that file said some version of "update CacheService" or "fix bug." Eleven commits, zero context, and the person who wrote them had already rolled off the project. I ended up re-reading the diffs line by line to reconstruct the reasoning myself.
That's the actual problem AI prompts for git commit messages solve, and it's not the problem most people think they're solving. The pitch everyone makes is "AI writes your commit messages so you don't have to think of one" — which is true, but also the wrong goal. A commit message generated from a raw diff without context is just as useless as "update stuff," except now it's confidently-worded useless. The real win is using AI to force out the why, not to automate away the what.
Why "Update Stuff" Commits Happen in the First Place
Nobody writes a bad commit message because they don't care. They write one because by the time git commit runs, the actual reasoning is gone from working memory — you were thinking about the bug, not about how you'd explain the bug to someone else six months from now. The diff is sitting right there, syntactically complete, and your brain treats "I changed these lines" as equivalent to "I know why I changed these lines." It isn't.
This is exactly where an AI prompt is useful, because an LLM reading a diff cold has the same blind spot you'd expect from a human skimming it: it can describe what changed accurately, but it has no idea why unless you feed it that context explicitly. Most people skip that step, pipe the raw git diff into a model, and get back a message that's technically correct and practically worthless — "Modified CacheService.php to add a null check." Great, I could read that from the diff myself.
The Prompt That Actually Produces a Useful Message
The fix is a prompt template that forces two inputs instead of one: the diff, and a one-line note from you about the reason — a ticket number, a bug symptom, a sentence. The model's job isn't to invent the why from nothing; it's to turn your terse reason plus the mechanical diff into a properly structured message.
You are writing a git commit message. Follow Conventional Commits format.
Context from the developer (this is the WHY — treat it as ground truth,
don't second-guess it): {{reason}}
Diff:
{{diff}}
Rules:
- First line: type(scope): short summary, max 72 chars, imperative mood
- Blank line, then 1-3 sentences explaining WHY this change was made,
using the developer's context above — not a restatement of the diff
- If the diff touches more than one logical concern, say so explicitly
rather than writing a message that only covers one of them
- Never invent a reason not present in the context field
That last rule is the one people skip, and it's the one that matters most. Without it, a model will happily fabricate a plausible-sounding justification for your change, and a confidently wrong "why" in your git history is worse than no why at all — it actively misleads the next person who reads it.
Wiring It Into prepare-commit-msg
Running this by hand defeats the point, so I wire it into Git's prepare-commit-msg hook. The hook grabs the staged diff, prompts for a one-line reason if one wasn't already typed, and calls the model:
#!/usr/bin/env bash# .git/hooks/prepare-commit-msgCOMMIT_MSG_FILE=$1DIFF=$(git diff --cached)# Skip if the diff is empty or this is a merge commit[ -z "$DIFF" ] && exit 0[ "$2" = "merge" ] && exit 0read -p "One-line reason for this change: " REASONPROMPT=$(cat <<EOFYou are writing a git commit message. Follow Conventional Commits format.Context from the developer (this is the WHY): ${REASON}Diff:${DIFF}Rules:- First line: type(scope): short summary, max 72 chars, imperative mood- Blank line, then 1-3 sentences explaining WHY, using the context above- Never invent a reason not present in the context fieldEOF)curl -s https://api.openai.com/v1/chat/completions \-H "Authorization: Bearer $OPENAI_API_KEY" \-H "Content-Type: application/json" \-d "$(jq -n --arg p "$PROMPT" '{model:"gpt-4o-mini",messages:[{role:"user",content:$p}]}')" \| jq -r '.choices[0].message.content' > "$COMMIT_MSG_FILE"
A few things about this specific setup matter more than the headline idea. It skips merge commits entirely — feeding a merge diff into this prompt produces garbage every time, because a merge diff isn't really "a change," it's two histories colliding, and the model has nothing coherent to explain. It also asks for the reason interactively rather than trying to infer it from the branch name or ticket number in a config file, because in practice developers forget to update that metadata and a stale ticket reference is worse than none.
Where This Actually Breaks Down
Most write-ups on this stop at "and now your commits are great," which isn't honest. There are two situations where I'd skip this entirely rather than force it.
The first is large, mixed diffs — a 400-line change touching six unrelated files. The rule about flagging multiple concerns helps, but it doesn't fix the underlying problem, which is that the commit itself should have been split before it ever got this far. I'd rather stop and run git add -p to break the change into logical commits than lean on the model to paper over a commit that's doing too much. If you're using this hook as a reason to stop splitting commits properly, it's making your history worse, not better.
The second is anything security-sensitive. I don't send diffs touching auth logic, credentials, or payment code through a third-party API as a matter of policy, full stop, regardless of how good the resulting commit message would be. For those commits I write the message by hand — it takes thirty extra seconds and it means I'm not deciding, diff by diff, which chunks of sensitive code are fine to send off-box.
One more thing worth being blunt about: treat the generated message as a draft, not a final answer. I read every single one before committing — it takes maybe five seconds — because I've had the model misread which function a null check actually guards against, and a wrong "why" that sounds authoritative is the one git-history failure mode that's genuinely worse than a lazy one-liner.
Conclusion
If you take one thing from this, make it the two-input rule: never let a commit-message prompt run on the diff alone, because a diff tells you what changed and nothing about why, and a model guessing at "why" will guess wrong often enough to actively hurt your git history. Feed it a one-line reason, skip it on merges and anything security-sensitive, and read the output before you commit — that's the version of this that actually makes your history useful six months later instead of just quieter today.


