Enforce commit quality with a commit-msg hook, not prompts. This hard gate forces Claude Code to explain 'why', yielding atomic commits and a searchable git history.
Key Takeaways
- Enforce commit quality with a commit-msg hook, not prompts.
- This hard gate forces Claude Code to explain 'why', yielding atomic commits and a searchable git history.
The Problem: Your Agent Is Destroying Your Git History
You 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, you don't think twice about the commit messages it generates. They're technically accurate and completely useless:
fix stuff
update code
wip
address feedback
Six weeks in, you need to find when a particular retry-handling bug was introduced. git log --oneline gives you forty lines of update code and fix stuff. git blame points you at a commit titled fix. You end up bisecting manually across a dozen commits that all look identical from the log.
That's when it clicks: 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 your agent is actively destroying that record every time it commits.
The Fix: Hooks Beat Prompts
The solution has four parts. The critical insight: prompts drift. Under time pressure (or a long context window), the agent slides back into fix stuff-style messages. You need a hard gate.
Step 1: Add a commit-msg hook
Move 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 with 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.
Step 2: Answer three questions before committing
Add an explicit instruction to your CLAUDE.md requiring the agent to answer three questions before staging:
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 matters more than you'd expect — it naturally enforces atomic commits. Once the agent has to justify a commit with a specific "why," bundling three unrelated fixes into one commit becomes obviously wrong.
Step 3: Generate messages AFTER the diff is final
Move message generation to after the test run, and give 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 closes a subtle gap: messages that describe what the agent meant to do instead of what actually landed in the diff.
The result
Here's the same change before and after the pipeline:
# 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 you, six weeks later, exactly why the change exists, what it replaced, and which test to trust if you touch this code again.
What to Expect
The hook will reject roughly one in five of your agent's first commit attempts in the first couple of days. That feels like friction. Don't loosen the rule — those rejections are the point. Every one of them is a commit that genuinely didn't have a clear "why" yet. The retry rate drops to under 5% within a week as the upstream prompt changes catch up with what the hook expects.
Treat the commit log as an interface, not an artifact. Think of git log as something your agent (and future-you) queries under pressure, the same way you'd think about API design. Optimize for the reader six weeks from now, not for the second it takes to generate the string.
Source: dev.to
Originally published on gentic.news

Top comments (0)