Tooling & Integration

Normalising Agent Commits: The Only Surviving Record of Intent

David Guzenburg/ / 8 min read

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.

gitcommitsconventionshooks

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.

Errors should teach

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

CheckCatchesSeverity
Subject matches the formatMalformed messagesFail
Body present for non-choreUnexplained changesFail
Subject contains " and "Commits that should be splitWarn
Agent: trailer presentMissing attributionWarn
Body references an issue for fixFixes with no traceable causeWarn

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.

Takeaway

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.

Keep reading
Workflow Architecture

Layering Agent Instructions: Personal, Project and Path-Scoped Rules

Three instruction channels teams routinely confuse, a test for which layer an instruction belongs in, and why explicit prohibitions outperform stated preferences.

Codex vs Claude

Generated Placeholder Assets: Borrowing Against a Design You Have Not Made

Agent-generated placeholder icons unblock a build and quietly ship to production. Path conventions no production file may import, a four-line CI gate, watermarks, and the provenance record that makes the licensing question answerable.

Codex vs Claude

Approval Fatigue: The Control Degrades Every Time You Use It

Interactive permission prompts spend a consumable resource. Why the count matters more than the wording, why deny lists beat allow lists, and how to make destructive commands break the rhythm instead of matching it.

Codex vs Claude

One Agent, Many Surfaces: They Share a Model and Nothing Else

Terminal, editor, desktop, browser and hosted runner are different tools sharing an account. What differs, why consistency has to come from the repository, verifying permission denials per surface, and choosing a surface by the shape of the task.

← LSP and Agents: Two Different Models of Understanding Code  ·  Static Analysis in the Loop: Linters as Feedback, Not Just Gates →

All tooling & integration articles  ·  Every article