How much of Antigravity's generated commit message I actually keep
Where the Review pane's generated commit message stops being enough, and where I start writing. A prepare-commit-msg hook that adds a Why trailer, plus measured behaviour during rebase, cherry-pick and revert.
Late one night at the end of August I was going back through the AdMob mediation setup in my wallpaper app. A change from six months earlier had lowered the priority of one adapter, and I could not remember why.
The commit message said Update mediation config and nothing else. Open the diff and you can see exactly what moved. Why it moved was nowhere.
I was the one who let that commit through. I read the drafted message, confirmed it matched the change, and committed. What bothers me is that I have no memory of cutting a corner.
Antigravity 2.0 brought version control into the Review pane, so staged changes now come with a generated commit message you can preview before committing. I'm glad it exists. I also think it makes that particular kind of loss easier to repeat.
So I drew a line. Whatever the diff can tell me later goes to the generator; whatever the diff throws away, I write myself. Here is how I put that into practice, including the point where my own hook stopped my work.
Agent Edits and Uncommitted split by origin, not by state
The Review pane offers three views: Agent Edits for what the agent touched, Uncommitted for the whole working tree, and Branch for branch state. Diffs render as split or unified, and staged changes get their own view.
That split is unusual for a Git client. Most tools divide by file state — untracked, modified, staged. Antigravity divides by where the change came from.
Here is what I misread at first. Agent Edits lists files the agent rewrote, which is not the same as files the agent meant to change.
If a formatter runs on save, lines you never asked about ride along. If a build output path is missing from .gitignore, artifacts ride along too. I once staged a whole Agent Edits view with an unrelated Xcode scheme diff sitting inside it.
What follows includes implementation code, benchmarks, and practical content we hope you'll find useful. This site runs without ads — server and development costs are supported entirely by members like you. If it's been helpful, we'd be truly grateful for your support.
WHAT YOU'LL LEARN
✦You'll be able to leave agent-written changes in a shape where, six months later, you can still recover the reasoning behind them
✦You'll be able to draw your own line between what the generated message may say and what you insist on writing yourself
✦You'll be able to keep a commit hook from stalling you mid-rebase or mid-cherry-pick, because the pass-through conditions are in place before you need them
Secure payment via Stripe · Cancel anytime
✦
Unlock This Article
Get full access to the rest of this article. Buy once, read anytime. This site is ad-free — your support goes directly toward keeping it running.
I settled on one test: six months from now, can I recover this from git log and the diff alone? If yes, the generator writes it. If no, I do.
Information in the message
Recoverable from the diff?
Who writes it
Files, functions, line counts
Yes
Generated
Kind of change (add, edit, delete)
Yes
Generated
Rough blast radius
Mostly
Generated
Why that particular value
No
Me
The option I rejected
No
Me
The condition for reverting
No
Me
This is not a complaint about generation quality. The summaries are more accurate than mine. The point is that a summary can only be built from the diff, so anything absent from the diff is out of reach for any model, however good.
In the mediation case, the fact of the change was in the diff. What was missing was the observation that fill was dropping in one region only, and the fact that I planned to restore the value in the next release.
So I fixed two lines of format. Why: carries the material behind the decision, Revert-if: carries the condition for undoing it. Both parse as Git trailers, which means I can pull them out mechanically later.
Put an empty Why line in prepare-commit-msg
I keep the generated message. The hook appends two empty fields underneath it, and stops the commit when they stay empty.
#!/bin/sh# .git/hooks/prepare-commit-msg# $1 = message file / $2 = message source / $3 = revisionMSG_FILE="$1"SOURCE="$2"GIT_DIR_PATH=$(git rev-parse --git-dir)# 1) Stay out of the way during replay and integration (see the measurements below)if [ -d "$GIT_DIR_PATH/rebase-merge" ] || [ -d "$GIT_DIR_PATH/rebase-apply" ] \ || [ -f "$GIT_DIR_PATH/CHERRY_PICK_HEAD" ] || [ -f "$GIT_DIR_PATH/MERGE_HEAD" ]; then exit 0ficase "$SOURCE" in merge|squash) exit 0 ;;esac# 2) A filled-in Why means there is nothing to doif grep -qE '^Why:[[:space:]]*\S' "$MSG_FILE"; then exit 0fi# 3) Keep the generated body, append the empty fieldsprintf '\nWhy: \nRevert-if: \n' >> "$MSG_FILE"# 4) One-shot -m commits never open an editor, so stop hereif [ "$SOURCE" = "message" ]; then echo "prepare-commit-msg: Why is empty. Use git commit instead of git commit -m." >&2 exit 1fiexit 0
Make it executable.
chmod +x .git/hooks/prepare-commit-msg
I checked the behaviour on git 2.34.1. git commit -m "add f" exits 1 at step 4. A -m message that already contains Why: passes at step 2. An editor commit opens with the generated summary and two empty fields below it.
I deliberately put the blocking logic in prepare-commit-msg rather than commit-msg. The latter validates after you type, so you get scolded once the writing is done. The former hands you the empty field before you start.
$2 cannot tell a hand-typed commit from a replayed one
My first version had no step 1. I assumed $2 would separate git commit -m from a rebase replay. It does not.
On the same git 2.34.1 I logged $2 and the state files under .git from inside the hook. The results:
Operation
Value of $2
State under .git
git commit -m
message
none
git rebase replay
message
rebase-merge and CHERRY_PICK_HEAD
git cherry-pick
message
CHERRY_PICK_HEAD
git revert --no-edit
message
none
git merge --no-ff
merge
MERGE_HEAD
git commit (editor)
empty string
none
with commit.template set
template
none
Rebase and cherry-pick both report message, exactly like a hand-typed commit. A hook that keys on $2 alone will therefore halt in the middle of a rebase the moment it replays commits made before the rule existed. I hit that while consolidating branches across several sites, and it is an unpleasant place to be interrupted.
The state files are what actually distinguish them. During a rebase, rebase-merge already exists; during a cherry-pick, so does CHERRY_PICK_HEAD. Step 1 reads those.
One row went against my expectation. git revert --no-edit does not expose REVERT_HEAD at the time the hook runs, so there is no condition to pass it through on. A revert is treated like any ordinary commit.
I decided to keep that rather than patch around it. A commit that undoes something is precisely where the reason matters later, and I would rather be stopped for a moment than let an empty field through.
Finer commits shift the weight toward Why
Something else came out backwards. I had assumed that splitting commits finely would make written reasoning unnecessary. The opposite happened.
When a nine-file agent diff is split into meaningful units, each commit gets small. The smaller the diff, the more obvious the "what" becomes. Subjects turn concrete — Bump AdMob adapter priority for JP — and the summary does its whole job on its own.
What remains is why that granularity, and why that order. As the split gets finer, the subject line becomes generator territory and the body becomes reasoning territory.
The reverse also holds: coarse commits are where generated summaries help least. Summarise nine files at once and you land near "updated several settings". A summary only works when the diff is already about one thing.
I ended up delegating the splitting itself to the agent and writing only the reasoning behind the split. My month of using Inline Edit and Agent mode side by side is in a field note from running my wallpaper app.
Decide now what you will type six months from now
When you fix a format, fix the retrieval at the same time. Leave it and you will end up with Why:, Reason: and WHY: mixed together, and no way to search them.
# List only commits that carry a reasongit log --grep='^Why:' --format='%h %ad %s' --date=short# Subject and reason on one line (Why parses as a trailer)git log --format='%h%x09%s%x09%(trailers:key=Why,valueonly=true)' | head -20# Follow a setting in and out of the tree, reading the reasoning around itgit log -S'adapter_priority' --format='%h %s%n %(trailers:key=Why,valueonly=true)'# Count recent commits that break the rulegit log -30 --format='%H' | while read h; do git log -1 --format='%B' "$h" | grep -qE '^Why:[[:space:]]*\S' || echo "no-why $h"done
Keeping %(trailers:key=Why,valueonly=true) usable pays off whenever you want a table later. For the trailer parser to see it, Why: and Revert-if: have to sit in the final block of the message with no ordinary prose after them. That is the other reason the hook appends rather than inserts.
Revert-if: earns its keep at reading time rather than writing time. When something goes wrong while widening a staged rollout from 5%, it narrows down which commit to pull first. On a day when crash-free users dips below the threshold, having the candidates written down changes how calm the next hour is.
What to look at next Monday
I stumbled by trying to do this all at once — pushing the hook to every repository and applying the rule retroactively, which is how I stalled a rebase. Now it only applies to new commits.
Start with one repository, one prepare-commit-msg, and only the empty-Why check. After a week, read git log --grep='^Why:' and see where you actually wanted the reasoning. Mine clustered in two places I had not predicted: configuration values, and reverts.
Antigravity Lab is ad-free, supported entirely by members like you. We publish practical guides daily with implementation code, benchmarks, and production-ready patterns. If you've found it useful, we'd love to have you on board.