Why git worktrees don't prevent agent conflicts
A worktree gives each agent its own copy of the files. Claude Code worktrees, Conductor and Claude Squad all build on this, so agents stop overwriting each other while they work. It never checks that their changes still fit together. We replayed 733 pairs of AI agent pull requests from public GitHub projects. Git found a textual conflict in 179 of them, and one of the pairs it merged cleanly no longer built.
The short answer
Two agents can collide in two ways. In a textual conflict, both agents edited the same lines, and Git stops the merge to ask what you want. In a semantic conflict, they edited different lines that depend on each other. Git merges those without a word, and the combined code is wrong.
Textual conflict
Both agents edit the same lines. Git stops the merge and asks you what to keep.
Semantic conflict
The agents edit different lines that depend on each other. Git merges them, and the combined code is wrong.
Worktrees prevent neither kind. They move both to the end, after the agents have finished and their context is gone. To catch a conflict earlier, a tool must compare the worktrees with each other while the agents still work.
What a worktree isolates
A worktree is a second working folder linked to the same repository. It has its own branch, its own staged changes and its own files on disk. It shares the commits and branches of the main checkout, so you don't clone the repository again.
git worktree add -b agent-a ../shop-agent-a main
git worktree add -b agent-b ../shop-agent-b main
git worktree list
Every popular way to run agents in parallel uses this. claude --worktree name makes a worktree under .claude/worktrees/ on a branch called worktree-name (Anthropic, Run parallel sessions with worktrees). Conductor makes one for each workspace under ~/conductor/workspaces/ (Conductor docs, Git worktrees). Claude Squad pairs a tmux session with a worktree for each agent (Claude Squad on GitHub).
This solves a real problem. Two agents in one folder save over each other's edits, and each one then "fixes" code it didn't write. With one worktree for each agent, that stops.
What it leaves open
Each agent now tests only its own copy. Your test suite runs on agent A's work and on agent B's work, but never on both together. Nothing in a worktree setup looks across to the other folders.
That matters because agent pull requests rarely arrive alone. Xu, Subramanian and Karthik studied 33,596 agent pull requests in 2,807 GitHub repositories. 79.4% of them were open at the same time as another agent pull request in the same repository (Xu et al., AI Agent Pull Requests on GitHub, arXiv 2607.04697).
How often real agent pairs collide
We used the same dataset (AIDev, from Hugging Face) and the same method as the paper. We took one pair of overlapping agent pull requests from each of 747 repositories. We fetched both heads from GitHub and merged them with git merge-tree. Twelve repositories were gone or private and two pull request refs were missing, so 733 pairs were left. For each pair that merged cleanly, we ran the project's own tests in a container, on pull request A, on pull request B and on the merge.
| What happened | Pairs |
|---|---|
| Git reports a textual conflict | 179 of 733 (24.4%) |
| Conflict when two different agents made the pull requests | 52 of 120 (43.3%) |
| Conflict when the same agent made both | 127 of 613 (20.7%) |
| Git merges cleanly | 554 of 733 (75.6%) |
| Clean merges where the tests could run on both sides and on the merge | 44 |
| Of those, the merged code fails | 1 |
The conflict rates match the paper closely. It reports 41.7% for pairs from different agents and 19.8% for pairs from one agent.
Only 44 clean pairs could be tested end to end. In 286 pairs we found no tests at the repository root. In 218 pairs, one of the pull requests already failed its own tests in a clean container. Most of those suites need a database, a browser or network access. One break in 44 is too small a sample to quote as a rate. It is still a clear case from real agents, a merge that Git accepted and the compiler rejected.
- 179pairs where Git stopped with a textual conflict
- 510clean merges whose tests we could not run on both sides
- 44clean merges tested end to end. One of them failed.
The clean merge that did not build
The pair is #72 and #73 in firecrawl-mcp-server, an MCP server for the Firecrawl web scraping API. Devin opened both with the same title, "Implement maxAge fast scraping parameter". Each one adds a maxAge option to the input schema of the scrape tool, the SCRAPE_TOOL object in src/index.ts.
timeout timeout: {
type: 'number',
...
},
+ maxAge: {
+ type: 'number',
+ default: 0,
+ },
actions: {location description: 'Location settings for scraping',
},
+ maxAge: {
+ type: 'number',
+ description: 'Maximum age in milliseconds ...',
+ },
},
required: ['url'],The two additions sit about a hundred lines apart in the same object. Git sees two separate hunks and merges them. The merged file then holds two maxAge keys in one object literal, and TypeScript refuses it.
src/index.ts(198,7): error TS1117: An object literal cannot have
multiple properties with the same name.
The project builds with tsc in its prepare script, so npm ci fails on the merge. Each pull request passes npm ci && npm test alone, and so does the base. A reviewer who reads either diff sees a correct change.
You can replay it yourself in a minute.
git init -q firecrawl && cd firecrawl
git fetch --filter=blob:none https://github.com/mendableai/firecrawl-mcp-server \
refs/pull/72/head:pr72 refs/pull/73/head:pr73
git merge-tree --write-tree pr72 pr73 # exit status 0, prints the merged tree id
git show <tree id>:src/index.ts | grep -n "maxAge: {"
The last command prints lines 95 and 198, both inside SCRAPE_TOOL. It also prints line 383, the crawl tool's own maxAge, which is a different object and fine.
Check two worktrees yourself
Plain Git can do part of this job. git merge-tree --write-tree merges two branches in memory and touches no worktree. It exits with 0 when the merge is clean and 1 when it has conflicts. Add --name-only to list only the files that clash.
git merge-tree --write-tree --name-only agent-a agent-b
echo $? # 0 clean, 1 conflict
A clean result tells you nothing about meaning, as firecrawl shows. To test the combination, merge it in a spare worktree and run the tests there.
git worktree add --detach ../combo agent-a
cd ../combo
git merge --no-edit agent-b && npm ci && npm test
cd - && git worktree remove --force ../combo
This works, but the work grows fast. Five agents make ten pairs, and each new commit from any agent makes its pairs out of date. Both commands also see only committed work. They cannot see an agent that is halfway through an edit, and a warning helps most at that time.
What gitta adds across the worktrees
gitta watches every worktree of the repository, whichever tool made it. It reads git worktree list, so worktrees from Claude Code, Cursor, Codex, Conductor or git worktree add all count. You run one command in the main checkout.
gitta init --gate "npm ci && npm test"
The gate is your own test command. gitta uses no AI model to judge the code. It parses each changed file with Tree-sitter. It finds the functions, classes, constants and fields that an agent changed, and what each agent's new code uses. It follows imports and calls up to three steps away.
When agent A changes something that agent B's new code uses, B gets a warning inside its own session at its next step. Claude Code gets it through hooks. Cursor and Codex get it through MCP tools that the agent calls. You can see the same picture, and test a combination, by hand.
gitta status # every agent, its files and its warnings
gitta check agent-b --with agent-a # PASSES, BREAKS or DOES_NOT_MERGE
gitta sync agent-b --from agent-a # bring A's current work into B's copy
You merge with git merge, git pull or git push, as always. gitta runs inside those commands. It runs the gate on the combined code before main moves, and it stops the merge when the tests fail. The merge output names the failing test and its first error line. When a function that the code calls changed on main, the output also shows its new definition.
What gitta reported on the replayed pairs
We ran gitta pr-check on every replayed pair, the same check that runs on pull requests in CI. On firecrawl, without any tests, it warned both edited: src/index.ts assign SCRAPE_TOOL (#72 + #73). With the gate it merged the pair, ran the install and reported a break with the same TS1117 error.
A warning means that two changes meet. It does not predict that the code breaks. In the first run, the check warned on 13 of the other 43 clean pairs, and the merge of each passed. In many of those, both pull requests edited a changelog, a CI workflow or a test function. Overlaps like these now show as info, which never fails the check. With the gate on, the check failed on no working pair in one run. In another run it failed on one flaky test.
On the conflict side, gitta never said "does not merge" for a pair that Git merged cleanly, across all 545 clean pairs where the check finished. On 20 pairs where Git reported a conflict, gitta's merge by structure combined the two sides, for example in rqlite and crewAI.
Keep your worktrees, and let gitta give you the view across them. To set it up with Claude Code, follow Run multiple Claude Code agents in parallel. For the details of warnings and sync, read How gitta works.