← Back to BlogEngineering

Clean Git History When an Agent Writes the Code

Agents default to one 500-line commit, which breaks git bisect, blame, and clean reverts. The discipline: atomic commits split by intent.

A healthy pull request is a handful of atomic commits, each one a single logical change that builds and passes on its own. AI coding agents default to the opposite: one 500-line commit that folds a refactor, a behavior change, a dependency bump, and three unrelated tweaks into a single blob. That blob is more than a readability problem. It quietly disables the three git tools engineers reach for when something breaks in production: bisect, blame, and a clean revert.

This is a history-structure problem, and it sits upstream of code review. A reviewer can approve a giant commit and still leave the team with a repository that cannot answer "which change introduced this bug" or "can we back out this one feature without losing the other four." The fix is not more review. It is teaching the agent, or the humans steering it, to draw commit boundaries around logical units, keep structural changes out of behavioral commits, and verify each unit before it lands.

The argument here favors a specific practice, and that practice points somewhere commercial: we build TLM Forge, a process layer that gates AI-written changes before they ship. So read the git mechanics as the load-bearing claims, verifiable against the official docs, and read the one product section knowing where it comes from.

Why one giant commit disables git

Git's most useful debugging tools all assume the commit is the unit of change. Break that assumption and the tools degrade from precise to useless. Start with bisect. The command "uses a binary search algorithm to find which commit in your project's history introduced a bug," narrowing a range of commits down to the first bad one in roughly log2(n) steps. Bisect can only be as precise as your commits are small. If the offending change is buried inside a 500-line commit alongside unrelated work, bisect will correctly point at that commit and leave you to read all 500 lines by hand, which is exactly the search bisect was supposed to save you from.

Sources: git-bisect documentation (git-scm)

The precision loss is measurable in effort. Bisect over 50 atomic commits isolates a regression to a few lines in about six steps. Bisect over a history where each commit bundles five concerns still takes six steps, then dumps a 500-line diff at the end and asks you to find the needle yourself. The tool is doing the same log-time search either way. Commit granularity decides whether its answer is actionable.

Blame degrades the same way. git blame "annotates each line in the given file with information from the revision which last modified the line," so you can jump from a suspicious line to the commit that wrote it and read why. When an agent squashes a refactor and a behavior change together, blame points every touched line at the same mega-commit, whose message says something like "implement feature and clean up." The annotation is technically correct and practically empty: it tells you which blob touched the line, not which intent.

Sources: git-blame documentation (git-scm)

Revert is where the cost turns into risk. git revert records "new commits to reverse the effect of some earlier commits," which is the safe way to back a change out of a shared branch. But revert operates on whole commits. If the bug lives in one feature that shares a commit with four healthy changes, reverting takes all five with it. You are then forced to choose between shipping the known bug and manually reconstructing four unrelated changes, under production pressure, which is the worst possible time to be hand-editing a diff.

Sources: git-revert documentation (git-scm)

Insight

A commit is not a save point. It is the granularity at which git lets you search, attribute, and undo. Every unrelated change you fold into one commit is a change you can no longer bisect to, blame precisely, or revert independently. The agent that writes one commit per session is trading three debugging tools for one keystroke saved.

Rule one: never mix structure and behavior

The single highest-value commit discipline is to separate changes that alter behavior from changes that only move code around. Kent Beck draws exactly this line, observing that "changes to the structure of the system are fundamentally different from changes to the behavior of the system," and that the thing separating them is reversibility. A structural change is cheap to reverse. As Beck puts it, "If I change the structure of a program, though, it's easy to reverse. Don't like my extracted helper function? Inline it." A behavior change ships real consequences and deserves more scrutiny, so keeping the two in separate commits lets you pass the reversible one through and concentrate on the risky one.

Sources: Kent Beck, Structure & Behavior

Agents violate this constantly. Asked to add a feature, an agent will rename three variables, extract a helper, reformat a file, and add the actual new logic, then commit the lot. The few lines that change behavior are now hidden inside a hundred lines of tidying. A reviewer cannot see the risky part, bisect cannot isolate it, and a revert of the feature drags the refactor back out with it. Split those into two commits, refactor first and behavior second, and every downstream tool regains its precision. The refactor commit should be provably behavior-preserving: same tests, same output. The behavior commit should be small enough to read in one sitting.

Pro Tip

Give the agent the ordering rule explicitly: "First commit any renames, extractions, and formatting as a single structural change that does not alter behavior, verified by the existing tests passing unchanged. Then commit the behavior change on top." This one instruction removes the most common cause of unreviewable agent diffs, because the risky lines stop hiding inside the trivial ones.

Rule two: verify, then commit, per unit

An atomic commit is defined by more than size. It leaves the tree in a working state. A good atomic commit encapsulates one logical unit of change, leaves the codebase in a working state with all tests passing, and does not mix concerns. That working-state property is what makes bisect trustworthy: bisect assumes it can build and test any commit it lands on, and if half your history does not compile, bisect wastes steps on commits it has to skip.

The practice that produces working commits is verify-then-commit at the granularity of a logical unit, not at the end of a session. Finish one coherent change, run the build and the relevant tests, commit it, and only then start the next. Agents tend to do all the work first and commit once at the end, which is why their single commit is both huge and unverified at intermediate points. There is no working state between the empty branch and the finished blob, so there is nothing for bisect to land on and nothing a reviewer can check incrementally. A green suite on each commit is a floor, not proof of correctness, because agents can write tests shaped to the code they already intend to produce. Testing AI-generated code covers why the suite passing is where verification starts rather than ends.

Rule three: messages capture intent, not the diff

A commit message exists to tell a future responder why the change was made, because the diff already shows what changed. The canonical guidance is blunt about this: "Explain the problem that this commit is solving. Focus on why you are making this change as opposed to how (the code explains that)." Agents produce the opposite by default. Their messages restate the diff, "update handler, add null check, refactor loop," which is information the reader can already see and none they need.

Sources: Chris Beams, How to Write a Git Commit Message

The value of intent shows up at the exact moment a giant commit fails you: two months later, when someone bisects to a commit and has to decide whether the change is safe to revert. A message that says "clamp retry count to 5 because the upstream rate limiter returns 429 above that, see incident 4821" lets the next engineer act. A message that says "fix retries" sends them reading code and guessing. This is why the message and the boundary are the same discipline: a commit scoped to one intent can have a message that states that intent, and a commit that bundles five intents cannot have an honest subject line at all.

Insight

Test a commit message by deleting the diff and asking whether a teammate could reconstruct why the change was needed. "Fix bug" fails. "Reject empty display names because the profile renderer crashes on a zero-length string" passes. The agent has the reasoning in its context while it works. The message is the only place that reasoning survives after the session ends.

Rule four: boundaries match reviewable units

Commit boundaries should line up with the units a human will review, because the commit is the smallest thing a reviewer can accept or reject. When one commit carries a schema migration, an API change, and a UI tweak, the reviewer has to approve all three or block all three, even though they carry different risk and might need different eyes. Split by concern and the migration can get database review, the API change can get a contract check, and the UI tweak can merge on green. The boundaries you draw at commit time decide how granular the review can be, which is upstream of the pull request itself. Keeping AI pull requests small covers the review side of the same discipline, and it starts here, at the commit.

This is also where decomposition and history meet. If you cannot describe a change as a short sequence of single-intent commits, the task was probably underspecified, and the agent filled the gap by doing several things at once. A precise spec that names the one behavior to change produces a diff that falls naturally into clean commits. Spec-driven development is the front end of good history: the clearer the unit of work going in, the cleaner the commit boundary coming out.

Git operationOne 500-line commitAtomic commits split by intent
git bisectLands on the mega-commit, then dumps 500 lines to search by handIsolates the regression to a few lines in one commit
git blameEvery line points at one blob message like "implement feature"Each line points at the commit and intent that wrote it
git revertBacking out one feature drags four unrelated changes with itRevert the one bad commit, leave the rest untouched
Code reviewApprove or block all concerns together, risky lines hiddenReview each concern at its own risk level
Production rollbackChoose between shipping the known bug or hand-editing a diffRevert the single commit, ship, investigate calmly

Why agents drift back to the blob

Agents lose the reasoning behind their own commit boundaries the moment a session ends. The decision that the refactor and the feature were two separate changes, the intent behind each one, the reason a file was off-limits: none of that survives into the next session unless something stores it. A private, persistent memory layer such as MemX keeps those decisions durable across runs, so a follow-up session continues the same clean history instead of squashing its work into one commit because it has forgotten why the boundaries existed.

The deeper reason is that nothing in the default agent loop rewards clean history. The agent optimizes for a working end state, and one commit reaches a working end state as surely as ten do. Commit discipline has to be imposed by the process around the agent, the same way tests and review are. This is the gap TLM Forge is built for: a spec audit fixes the unit of work before code exists, so the change has a natural boundary; phase-gated verification means each logical unit is built and checked before the next begins, which produces working commits instead of one final blob; and independent review agents that did not write the diff read the history for mixed concerns before an adversarial convergence gate allows the change to ship. None of that makes the agent write better commits by itself. It makes clean history a requirement the change has to meet rather than a habit you hope the model has.

A squashed commit is not always wrong: some teams squash every pull request to one commit on merge by policy, and that is a deliberate trade that keeps main linear at the cost of granularity. The claim here is narrower. Whatever your merge strategy, the work itself should be built and verified as a sequence of single-intent, structure-or-behavior commits, because that is what lets bisect, blame, and revert do their jobs during the work and during the incident. What you do to that history at merge time is a separate decision, made with eyes open.

Frequently asked questions

01Why do AI coding agents make one giant commit?

Agents optimize for a working end state, and a single commit reaches that state as surely as many do. Nothing in the default loop rewards small commits, so the agent does all the work and commits once. Clean history has to be imposed by the process around the agent, like tests and review.

02How does a big commit break git bisect?

Bisect uses binary search to find the commit that introduced a bug. It can only be as precise as your commits are small. If the bad change is buried in a 500-line commit alongside unrelated work, bisect correctly points at that commit and still leaves you to search all 500 lines by hand.

03Should refactoring and behavior changes be in separate commits?

Yes. Kent Beck separates structural changes from behavior changes because they differ in reversibility: structural changes are cheap to reverse, behavior changes carry real risk. Splitting them into separate commits keeps the risky lines out of the tidying, so reviewers can see the behavior change and revert can target it alone.

04What should an AI agent write in a commit message?

The why, not the what. The diff already shows what changed, so the message should explain the problem the commit solves and the reasoning behind it. "Fix retries" fails. "Clamp retries to 5 because the upstream limiter returns 429 above that" lets a future responder act without re-deriving the intent.

05Is it fine to squash AI commits on merge?

Squashing at merge time is a valid policy that keeps the main branch linear. The separate question is how the work is built: as single-intent, verified commits, so bisect, blame, and revert work during development and during an incident. Decide the merge strategy with that trade in view.

Ship AI-written code you can trust

TLM Forge is the missing process layer for Claude Code: a spec audit, independent multi-agent review, enforced TDD, and an adversarial red-team gate.

Get TLM Forge