git add -A Will Happily Commit Your Conflict Markers
We broke our own command-line tool on a Sunday, and the bug was hiding inside a check we trusted.
Here's the setup. We were merging a backlog — about twenty-five pull requests — in one batch. Lots of them touched the same handful of core files, so conflicts were guaranteed. The merge script did the obvious thing: for each conflicted file, pick a side (git checkout --ours or --theirs), stage it, move on. Then, to be tidy, a single git add -A at the end to sweep up everything. Then commit. Then squash-merge to main.
It looked clean. The script even checked for unresolved files before committing:
git diff --diff-filter=U --name-only
Empty output. No conflicts. Ship it.
Fourteen files landed on main with <<<<<<< still in them.
The check was real. It was just checking the wrong thing.
Here's the part that cost us the afternoon. git diff --diff-filter=U lists files git considers unmerged. The moment you git add a file, git marks it resolved in its index — whether or not the file's contents still contain conflict markers. add doesn't read your file looking for <<<<<<<. It just flips a bit.
So when one of those per-file git checkout --ours calls silently failed (wrong path, already-staged state — it doesn't matter which), the file kept its markers. Then the catch-all git add -A staged it anyway. And --diff-filter=U — the one guardrail in the script — now reported a clean tree, because as far as the index was concerned, the conflict was resolved. Marked resolved. Markers intact.
The files were our core CLI internals — the database layer, the planner, the queue. The next time anyone ran the tool, it tried to parse <<<<<<< HEAD as code and fell over.
Three rules we now treat as non-negotiable
1. Never git add -A to finish a merge. Stage only the specific files you explicitly resolved, by name. The blast radius of -A is "every change in the tree," and during a conflict resolution that's exactly the set you can't afford to wave through blind.
2. Grep for markers before every commit. It's not the same check as "unresolved files." One literal scan across the tree:
git grep -nE '^(<{7}|={7}|>{7}|\|{7})' || echo "clean"
This reads contents, which is the thing --diff-filter=U doesn't do. It's three seconds and it would have caught all fourteen files.
3. Run the thing before you push. Not the tests — the actual binary, the actual CLI, the actual import. A file full of conflict markers doesn't fail a type check in a dynamic language; it fails when something executes it. The cheapest possible smoke test (tool --help, import the module, boot the server) catches an entire class of "looks committed, doesn't run" bugs.
Recovering without making it worse
Once bad commits are on main, the instinct is to force-push the history clean. Don't — not on a shared branch. We fixed it forward instead: found the last commit where the core files were known-good, restored those specific files from it, stripped the leftover markers, re-ran the CLI to confirm it booted, and opened a normal pull request with the fix. History stayed intact; nobody had to re-clone; the broken window was shut in one reviewable change.
The meta-lesson
The bug wasn't "we forgot to check for conflicts." We did check. The bug was trusting a check that answers a subtly different question than the one we were asking. --diff-filter=U answers "does git's index think anything is unmerged?" We needed the answer to "does any file on disk still contain conflict markers?" Those are usually the same answer. The afternoon we lost was the afternoon they weren't.
We build a lot of our systems to run themselves — agents merging their own work, queues feeding themselves. That only stays safe if the guardrails check the real thing, not a proxy for it. So now: stage by name, grep the contents, run the binary. Every time.
We write these up as we hit them. If you're building automation that commits its own code and you want a second set of eyes on the failure modes, come talk to us.
Found this useful?
Related posts
We Gave Every Agent Its Own Worktree. They Still Shared One Database.
We spent weeks making sure no two agents could touch the same files. Then two of them quietly corrupted the one thing we forgot they shared: the database. Here's the bug, the fix, and the rule we now live by.
The Self-Verifying Loop: Making "Builder ≠ Verifier" a Mechanical Property
There is a failure mode that every team running agents eventually hits, and almost nobody catches early: the agent that grades its own homework. You ask a model to do the work and to confirm the work is done, and it does…
A Green Check Is Not Proof: How We Learned Our Quality Gates Were Lying to Us
We run an autonomous software company where AI agents build and ship the work, and a set of "quality gates" decides when a piece of work is allowed to advance. For a long time those gates told us everything was fine. The…
The Queue That Resurrected Dead Work
We killed a product. The queue didn't get the memo — so it kept building it anyway, across three or four teams at once. Here's the bug, the fix, and the rule we now live by.