DEV Community

Cover image for Claude Code and Codex Solved the Same Ticket. Compare Both Branches With git range-diff
Gabriel Anhaia
Gabriel Anhaia

Posted on AI-assisted

Claude Code and Codex Solved the Same Ticket. Compare Both Branches With git range-diff


The ticket said: add soft delete to orders. Claude Code took it on branch agent-a, Codex took it on agent-b, and both reported the work done. So you compare the branches the obvious way:

git diff agent-a agent-b
Enter fullscreen mode Exit fullscreen mode

Git does what you asked. It compares the two branch tips (git diff docs), so every line Claude added that Codex didn't shows up as a deletion, and every line Codex added that Claude didn't shows up as an addition. Lines both agents wrote the same way disappear. You are reading "undo Claude, then apply Codex" as one diff, with the two approaches braided together line by line.

Git has had a command for this question since version 2.19: git range-diff. It pairs the commits on two branches and diffs the patches, not the files.

Set up the race: one ticket, two worktrees

The ticket in full: DELETE /orders/:id hides an order instead of removing the row, and GET /orders skips hidden ones. It needs a schema change, which is where two agents tend to make different calls.

Give each agent its own branch and folder with git worktree:

cd ~/code/app
git worktree add -b agent-a ../app-agent-a main
git worktree add -b agent-b ../app-agent-b main
Enter fullscreen mode Exit fullscreen mode

-b creates the branch at main and checks it out in the new folder. By default git refuses to check out a branch that another worktree already has, so the two agents can't end up sharing one.

A worktree is a fresh checkout, so gitignored files such as .env aren't in it (Claude Code worktree docs). Copy what your app needs and install dependencies in both folders, or the agents' test runs fail for reasons that have nothing to do with the ticket.

Now start the agents. In the first terminal, Claude Code takes the ticket as its first prompt with claude "query":

TICKET='Add soft delete to orders: DELETE /orders/:id
sets a flag instead of removing the row, GET /orders
skips deleted orders. Write a migration and tests.
Commit your work when the tests pass.'
cd ~/code/app-agent-a && claude "$TICKET"
Enter fullscreen mode Exit fullscreen mode

In the second terminal, cd ~/code/app-agent-b, run codex and paste the same text. Same words, same starting commit, so any difference between the branches comes from the agents.

Claude Code's own --worktree flag does this for Claude sessions, under .claude/worktrees/<name>. With two vendors, plain git worktrees keep both folders under one naming scheme.

The last line of the ticket matters: range-diff compares commits, not working trees. If an agent stops with uncommitted changes, run git -C ../app-agent-b status, read what's there, and commit it yourself.

First pass: what did each agent touch?

Before reading any code, get the shape of each answer:

git log --oneline main..agent-a
git log --oneline main..agent-b
git diff --stat main...agent-a
git diff --stat main...agent-b
Enter fullscreen mode Exit fullscreen mode

Two dots in git log list the commits on the agent's branch that main doesn't have. Three dots in git diff compare the branch with the point where it split from main, so you see only what the agent changed (git diff docs).

Read the stat for three things. Does each branch have a migration file? Does each have tests? And which files are on one list but not the other? If one agent touched a handful of files and the other touched a dozen, including a lockfile or CI config, you already have a question for the second branch.

Two branches growing from one trunk, with burnt-orange threads pairing matching commit beads and one unmatched bead on each side
range-diff pairs commits across two branches by comparing their patches.

git range-diff: pair the commits, diff the patches

Both branches start at main, so either form works:

git range-diff main..agent-a main..agent-b
# same thing, shorter:
git range-diff main agent-a agent-b
Enter fullscreen mode Exit fullscreen mode

The three-argument form is documented as equivalent to passing <base>..<rev1> and <base>..<rev2> (range-diff docs). This is the example output from those docs, and it shows every kind of line you can get:

-:  ------- > 1:  0ddba11 Prepare for the inevitable!
1:  c0debee = 2:  cab005e Add a helpful message at the start
2:  f00dbal ! 3:  decafe1 Describe a bug
  @@ -1,3 +1,3 @@
   Author: A U Thor <author@example.com>

  -TODO: Describe a bug
  +Describe a bug
  @@ -324,5 +324,6
    This is expected.

  -+What is unexpected is that it will also crash.
  ++Unexpectedly, it also crashes. This is a bug, and the jury is
  ++still out there how to fix it best. See ticket #314 for details.

    Contact
3:  bedead < -:  ------- TO-UNDO
Enter fullscreen mode Exit fullscreen mode

(The docs indent the inner block by four spaces. I trimmed it to two so the lines fit here.)

Each header line pairs a commit from the first range (left) with one from the second (right):

  • = means the two patches are the same.
  • ! means the commits correspond but differ, and the indented block under it is the diff between the two patches.
  • > with dashes on the left is a commit that only the second range has.
  • < with dashes on the right is a commit that only the first range has.

Inside a ! block, read the first column as range-diff's marker and the second as the original patch's. -+What is unexpected... is a line the first patch added and the second didn't. ++Unexpectedly, it also crashes... is a line only the second patch added. In your race, -+ lines are Claude's and ++ lines are Codex's.

The block also starts with author and message lines, because range-diff compares "the author information, the commit message and the commit diff" when it decides which commits correspond. Two agents rarely write the same commit message, so skip past that part of each pair.

When the two answers won't pair

Two independent solutions can come back as a < line and a > line with no ! between them. range-diff decided the patches were too different to call them two versions of one commit. That is already an answer: the agents chose different designs.

To see the patches diffed anyway, raise the creation factor. It defaults to 60, and the docs say to try a larger value when range-diff wrongly treats a large change as a total rewrite:

git range-diff --creation-factor=100 main agent-a agent-b
Enter fullscreen mode Exit fullscreen mode

If they still don't pair, raise it further.

Commit counts cause the other mismatch. If Claude made three commits and Codex made one, at most one pair can form. Compare squashed copies instead, without touching either branch:

base=$(git merge-base agent-a agent-b)
a=$(git commit-tree 'agent-a^{tree}' -p "$base" -m squashed)
b=$(git commit-tree 'agent-b^{tree}' -p "$base" -m squashed)
git range-diff "$base" "$a" "$b"
Enter fullscreen mode Exit fullscreen mode

git commit-tree creates a new commit object from a tree and prints its id. Each of these holds one branch's final files on top of the shared base, with the same message, so the message noise is gone too. No branch points at them, and the agents' branches stay as they were.

range-diff also takes a pathspec, which limits both ranges to those paths. For this ticket, start with the schema:

git range-diff --creation-factor=100 "$base" "$a" "$b" \
  -- db/migrations
Enter fullscreen mode Exit fullscreen mode

Two independent migrations are small patches that share little, so they may still come back as a < line and a > line. If they do, read them side by side with git diff main...agent-a -- db/migrations and the same for agent-b.

Read the migration before the code

Say Claude's branch stores a timestamp and Codex's stores a flag:

-- agent-a
ALTER TABLE orders ADD COLUMN deleted_at timestamptz;

-- agent-b
ALTER TABLE orders
  ADD COLUMN is_deleted boolean NOT NULL DEFAULT false;
Enter fullscreen mode Exit fullscreen mode

Neither one rewrites the table. The PostgreSQL 18 docs say a non-volatile default is stored in the table's metadata, and a column with no default is NULL, and "in neither case is a rewrite of the table required." So the choice is about meaning: deleted_at records when the order was hidden, and the flag doesn't.

In each branch's migration, look for:

  • A volatile default such as clock_timestamp(). Per the same page, that rewrites the whole table and its indexes.
  • A plain CREATE INDEX on orders. A standard index build "locks out writes (but not reads) on the table until it's done" (CREATE INDEX docs).
  • CREATE INDEX CONCURRENTLY inside a migration your tool wraps in a transaction. It cannot run inside a transaction block, so the migration fails when it runs.
  • A query change that filters on the new column without an index that matches the filter.

Run the tests on both, then land one

From the main checkout, run each branch's tests in its own folder. Swap npm test for your own test command:

for br in agent-a agent-b; do
  echo "== $br"
  git diff --stat main..."$br" | tail -n 1
  if (cd "../app-$br" && npm test); then
    echo "$br: tests pass"
  else
    echo "$br: tests fail"
  fi
done
Enter fullscreen mode Exit fullscreen mode

A branch with failing tests is out. Between two passing branches, take the migration you'd run against a busy orders table, then the smaller diff with fewer surprise files. If range-diff paired the commits, you are choosing between two styles of one fix. If it didn't, you are choosing a design.

Land the winner and clean up:

git merge --no-ff agent-a
git worktree remove ../app-agent-a
git worktree remove ../app-agent-b
git branch -d agent-a
git branch -D agent-b
Enter fullscreen mode Exit fullscreen mode

git worktree remove refuses a folder with modified or untracked files unless you pass --force (docs). Leave the flag off, so leftover work stops you. -D is for the losing branch, which was never merged.

The same race in Hermes IDE

I build Hermes IDE, so weigh this section with that in mind. Press ⌘N (Ctrl+Shift+N on Windows and Linux), type the ticket, pick Claude Code, and launch. Do it again with Codex. Each task starts on its own hermes/<name> branch in its own worktree, with the ticket as the agent's first prompt. New worktrees get their dependencies in seconds and their own ports, which covers the install half of the setup step above.

The Review Desk's Worktrees tab listing one Hermes worktree each for orders-api, web-app and orders-types under ~/code
The Review Desk's Worktrees tab: one Hermes worktree each for orders-api, web-app and orders-types, with each worktree's hermes/ branch, task, age and disk size, and the free disk space.

The branches are plain git, so everything in this post works on them. git branch --list 'hermes/*' gives you the two names, and git range-diff main hermes/<one> hermes/<other> compares them. Hermes doesn't run range-diff for you.

What it adds is the other half of the review. Every agent turn is saved, so you can diff or restore a single turn, with undo. ⌘G (Ctrl+Shift+G) opens the Review Desk, which shows each agent's changes by turn or by file, with risk flags, and sends a line comment back to the agent that wrote the line. range-diff tells you how the two answers differ. The turn view tells you how each agent got to its answer.

The Review Desk in By turn view, with two turns listed, turn 1's diff of src/db.ts and a drafted commit message
The Review Desk (⌘G) in By turn view for a Claude Code task in orders-api: two turns, turn 1's diff of src/db.ts adding a refunded status, the changed-files list, and a commit message drafted from the turns.

When you've picked, the Land button commits, opens a pull request or merges locally into the branch the task started from, and can be undone. Your own checks can decide when a task counts as done. Hermes is free, source-available under BSL 1.1, and runs on macOS, Windows and Linux. It isn't affiliated with Anthropic, OpenAI or Nous Research. Download it here, and the source is on GitHub.


If this was useful

The git half of this race is mechanical once you know range-diff. The hard call is the schema: a timestamp or a flag, an index now or later, a migration you can run on a busy table. Database Playbook is my short guide to choosing the right store, and the right shape, for the system you're building. It's one of 8 short Pocket Guides for Developers, on agents, RAG, prompts, observability, events, databases and system design: the whole series is here.

Database Playbook: Choosing the Right Store for Every System You Build

Top comments (0)