TL;DR
My autonomous coding agent used to commit with messages like fix stuff and update code. I built a small review step that forces it to explain why before it's allowed to commit, and enforced a lightweight convention on top. Here's what broke, what worked, and the five things I'd tell anyone letting an AI agent touch their git history.
The Problem
I run Claude Code in a semi-autonomous loop — it picks up tasks, writes code, runs tests, and commits when things pass. For the first few weeks I didn't think twice about the commit messages it generated. They were technically accurate and completely useless.
fix stuff
update code
wip
address feedback
Six weeks in, I needed to find when a particular retry-handling bug had been introduced. git log --oneline gave me forty lines of update code and fix stuff, no dates I could correlate against, and no hint of which of the twelve "wip" commits actually mattered. git blame pointed me at a commit titled fix. I ended up bisecting manually across a dozen commits that all looked identical from the log.
That's when it clicked: a human writing a bad commit message is annoying. An agent writing thousands of bad commit messages over months is a debugging tax that compounds. The commit log is the only durable record of why changes happened — and my agent was actively destroying that record every time it committed.
It's also worth saying why this is easy to miss at first. When you're pairing with an agent interactively, you see the reasoning live in the terminal — you don't need the commit message to remember why a change happened, because you were there. The problem only shows up later, once the agent is running unattended for hours or days and the commit log becomes the only trace of what it did and why. By the time I noticed, I had roughly three weeks of history that was functionally a black box.
How I Solved It
Step 1: Stop letting the agent free-write commit messages
The first fix was embarrassingly simple: I added an explicit instruction requiring the agent to answer three questions before staging a commit, not just describe the diff.
Before committing, answer:
1. What was broken or missing? (the "why")
2. What's the smallest correct fix? (the "what")
3. Does this change deserve its own commit, or does it belong
with pending work already staged?
That third question mattered more than I expected — see the "unbundle" lesson below.
Step 2: Enforce a convention with a git hook, not a prompt
Prompts drift. Under time pressure (or a long context window), the agent would slide back into fix stuff-style messages. I moved enforcement out of the prompt and into a commit-msg hook that rejects anything that doesn't fit a Conventional Commits shape and a minimum body length:
#!/usr/bin/env bash
# .git/hooks/commit-msg
msg_file="$1"
subject=$(head -n1 "$msg_file")
if ! echo "$subject" | grep -qE '^(feat|fix|refactor|test|chore|docs)(\(.+\))?: .{10,}'; then
echo "❌ Commit subject must match: type(scope): description (10+ chars)"
exit 1
fi
body_lines=$(tail -n +3 "$msg_file" | grep -c '.')
if [ "$body_lines" -lt 1 ]; then
echo "❌ Commit needs a body explaining *why*, not just *what*"
exit 1
fi
This is the single change that had the biggest effect. The agent doesn't get to skip the "why" — the hook hard-fails the commit and the agent has to retry with more context, which usually means going back and actually articulating the reasoning it already had.
Step 3: Feed diff + failing test into the message generation step
Early on the agent would write the message before running tests, based on its intended change rather than the actual diff. I moved message generation to after the test run, and gave it the actual git diff --staged plus the specific test output that motivated the change:
flowchart LR
A[Agent writes code] --> B[Run tests]
B -->|pass| C[git diff --staged]
C --> D[Generate commit message
from diff + test context]
D --> E[commit-msg hook validates]
E -->|reject| D
E -->|pass| F[Commit]
This closed a subtle gap: messages that described what the agent meant to do instead of what actually landed in the diff.
To make this concrete, here's the same change before and after the pipeline was in place:
# Before
commit a1b2c3d
fix stuff
# After
commit f9e8d7c
fix(retry): back off exponentially on 429s instead of fixed 1s delay
The payment sync job was hammering the upstream API immediately after
a 429, tripping the provider's abuse detector and extending outages.
Switched to exponential backoff with jitter, capped at 60s. Verified
against test_retry_backoff_caps_at_60s, which was previously flaky
because it asserted on a fixed delay.
The second version tells me, six weeks later, exactly why the change exists, what it replaced, and which test to trust if I touch this code again. That's the bar I now hold every agent commit to.
Step 4: One logical change per commit
The convention forced a side effect I didn't plan for: once the agent had to justify a commit with a specific "why," bundling three unrelated fixes into one commit became obviously wrong — there's no single coherent answer to "why" for three unrelated changes. I added an explicit rule: if the agent can't state one reason for the whole staged diff, it has to split it.
Lessons Learned
- A bad commit message from an agent isn't a style nitpick — it's a lost debugging clue. Humans write bad messages occasionally; agents write them constantly, and the log degrades fast.
-
Hooks beat prompts for enforcement. Prompt instructions compete with everything else in context and lose over long sessions. A
commit-msghook that rejects non-conforming messages is a hard gate the agent can't quietly ignore. - Generate the message after the diff is final, not before. Messages written from intent instead of the actual diff drift from reality the moment the agent iterates.
- Forcing a "why" naturally enforces atomic commits. I didn't set out to fix commit granularity — it fell out of requiring a coherent justification for every commit.
-
Treat the commit log as an interface, not an artifact. I now think of
git logas something my agent (and future-me) queries under pressure, the same way I'd think about API design. Optimize for the reader six weeks from now, not for the second it takes to generate the string.
There's a sixth lesson I almost left out because it's less flattering: the hook rejected roughly one in five of the agent's first commit attempts in the first couple of days after I turned it on. That felt like friction at the time, and I nearly loosened the rule to stop the retries from slowing things down. In hindsight the rejections were the point — every one of them was a commit that genuinely didn't have a clear "why" yet, usually because the agent was still mid-way through reasoning about the change. The hook was catching exactly the kind of half-formed commit I'd wanted to eliminate. The retry rate dropped to under 5% within a week as the upstream prompt changes (Step 1 and Step 3 above) caught up with what the hook expected.
What's Next
I'm working on having the agent flag its own commits that touch security-sensitive paths (auth, secrets handling, permission checks) with a stricter message template and an extra self-review pass before those specific commits land. I'm also experimenting with having the agent link each commit back to the specific test or log line that motivated it, so a future "why did this change" question can be answered without leaving the terminal. If either of those goes well I'll write them up separately.
A Note on Cost
None of this is free in the "agent time" sense. The retry loop from the hook rejections cost extra tokens and extra wall-clock time on every commit that got bounced back. I was initially worried this would meaningfully slow down the agent's throughput. In practice it added maybe 10-15 seconds per rejected commit — negligible next to the hours I've since saved not having to manually reconstruct "why" from a diff and a vague message. If you're tracking token spend per task the way I am, budget for this: enforcement that runs before a commit lands is cheaper than the archaeology you'll do without it.
Wrap-up
If you're running any kind of AI coding agent against a real repo, check what your commit log actually looks like after a month — not the code, the log. If you've solved this differently (semantic commit bots, LLM-based changelog generation, whatever), I'd like to hear about it. Follow me here on Dev.to for the rest of this series, and if you want to try Claude Code yourself, it's worth the fifteen minutes to set up a hook like the one above before you let it loose.
Top comments (0)