DEV Community

gentic news
gentic news

Posted on • Originally published at gentic.news

Git Hook + Claude Code: How to Force Your AI Agent to Write Commit

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

Lines of Force (1979) // Bob Snyder American, born 1946

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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?
Enter fullscreen mode Exit fullscreen mode

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]
Enter fullscreen mode Exit fullscreen mode

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.
Enter fullscreen mode Exit fullscreen mode

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)