Normalising Agent Commits: The Only Surviving Record of Intent
Teams lint the code an agent writes and ignore the commits it produces. Then the history fills with 'Update files' and nobody can bisect anything.
Commits are the part nobody configures
Teams put real effort into linting the code an agent writes and none into the commits it produces. Then the history fills with messages like "Update files" and "Fix issue", and six months later nobody can bisect anything.
This matters more with agents than without, for a specific reason: the commit message is frequently the only surviving record of intent. The author cannot be asked. If the message says nothing, the reasoning is gone.
What a useful agent commit contains
fix(billing): cap partner-api retries at 2
The retry count was raised to 5 in #4471, which tripped the
partner-api rate limit (3/min per key) and blocked our key for
an hour during business hours.
Reverts to 2 and adds a comment explaining why the low number
is deliberate, so it does not get "fixed" again.
Refs: INC-2026-0412
Agent: assisted — prompt in PR description
Four things distinguish it from what you get by default. It states what changed and why in the subject. It records the incident that caused it. It explains the non-obvious constraint so the next person does not undo it. And it declares that an agent was involved.
Specify the format, then enforce it
## Commits
Conventional Commits: `type(scope): subject`
types: feat fix refactor test docs chore perf
subject: imperative, lower case, no full stop, under 60 chars
Body required for anything other than `chore` and `docs`:
- WHY, not what. The diff shows what.
- Name the incident, issue or ticket if there is one.
- If the change encodes a non-obvious constraint, say what it is.
Never:
- "Update files", "Fix bug", "Changes" — say which and why
- Listing every changed file. That is the diff's job.
- Claiming a fix is verified unless you ran the tests.
#!/bin/bash
# .git/hooks/commit-msg
MSG_FILE="$1"
SUBJECT=$(head -1 "$MSG_FILE")
if ! echo "$SUBJECT" | grep -qE \
'^(feat|fix|refactor|test|docs|chore|perf)(\([a-z0-9-]+\))?: .{1,60}$'; then
echo "Bad subject line: $SUBJECT" >&2
echo "Expected: type(scope): imperative subject under 60 chars" >&2
exit 1
fi
TYPE=$(echo "$SUBJECT" | cut -d'(' -f1 | cut -d':' -f1)
BODY=$(sed -n '3,$p' "$MSG_FILE" | grep -v '^#' | tr -d '[:space:]')
case "$TYPE" in
chore|docs) ;;
*) if [ -z "$BODY" ]; then
echo "A body is required for '$TYPE' commits. Explain why." >&2
exit 1
fi ;;
esac
The hook is the part that makes the instruction real. An agent asked to follow a format complies most of the time; a hook that rejects the commit produces an error it must fix, which is a different reliability class.
Notice the hook prints what was expected, not only that something was wrong. An agent that receives "Bad subject line" alone will guess; one that receives the expected pattern fixes it in one turn.
Declaring agent involvement
Worth doing, and worth being careful about how.
Trailers are the right mechanism — machine-readable, greppable, and they do not clutter the subject line. What you want later is the ability to answer "which commits were agent-assisted", for the measurement discussed elsewhere in this series.
Agent: assisted # agent wrote part; a human directed and revised
Agent: generated # agent produced it; a human reviewed
Agent: none # ordinary human commit
Two cautions. Do not use the author or committer field for this — attribution should stay with the person accountable for the change, and rewriting it destroys blame and confuses every tool that reads it. And keep the vocabulary to three values; a richer taxonomy stops being applied consistently within a month.
Splitting what arrives as one commit
Agents produce large single commits. A session that fixed a bug, tidied two adjacent functions and reordered some imports commits all of it together, and the result is unbisectable.
## Commit granularity
One commit per logical change. If your work included a fix AND a
refactor AND a formatting pass, that is three commits.
- Formatting and import reordering go in their own `chore` commit,
never mixed with a behaviour change.
- Extracting a function is `refactor` and must not change behaviour.
- If you cannot describe a commit in one subject line without
using "and", it should be more than one commit.
The last line is the practical test, and it is the one to state, because it is checkable by the agent itself before committing.
Generating the message from the diff
Since the agent has the diff and the reasoning in context at commit time, it is better placed to write the message than anyone will be later. The prompt matters.
Write the commit message for the staged changes.
- Subject: type(scope): imperative, under 60 chars.
- Body: why this change exists. Not a list of what changed.
- If this fixes something that was previously done differently,
say why the previous approach was wrong.
- If the change encodes a constraint that is not obvious from
the code, state it so nobody undoes it.
- Do not claim tests pass unless you ran them in this session.
Output the message only.
The final constraint prevents a specific and corrosive habit: commit bodies asserting "all tests pass" written by a session that never ran them. Once a few of those exist, the claim stops meaning anything anywhere in your history.
Branch names too
The same discipline applies one level up, for the same reason: a branch
called fix or agent-work-3 tells a reviewer nothing
and does not survive as a record.
## Branches
`type/short-description` — same types as commits.
fix/partner-api-retry-cap
feat/tier-rate-limits
refactor/extract-allocation
Never: `patch-1`, `agent`, `wip`, your name, a ticket number alone.
A ticket number is not a description; put it in the body.
What to check in CI
| Check | Catches | Severity |
|---|---|---|
| Subject matches the format | Malformed messages | Fail |
| Body present for non-chore | Unexplained changes | Fail |
| Subject contains " and " | Commits that should be split | Warn |
Agent: trailer present | Missing attribution | Warn |
Body references an issue for fix | Fixes with no traceable cause | Warn |
Keep the failures to the two mechanical checks and leave the judgement calls as warnings. A CI job that fails because a subject line contains the word "and" will be disabled within a week, and you will lose the two checks that were worth having.
The commit message is often the only surviving record of why an agent-written change exists, since the author cannot be asked. Specify the format, enforce it with a hook whose errors say what was expected, record involvement in a trailer rather than the author field, and require one logical change per commit — with "can I describe it without saying and" as the test.