Quick answer
Two AI agents should not write in the same checkout. They share one index, one working directory, generated files, caches, and often the same formatter or test artifacts. One agent can stage, rename, regenerate, or delete files while the other is still reasoning about an older filesystem state. The result may be a conflict, but the more dangerous result is a clean-looking diff that silently mixes both tasks.
Use one Git worktree and one branch per agent. Start every branch from the same reviewed commit, assign non-overlapping write sets, and merge into an integration branch one at a time. Worktrees isolate files; they do not remove semantic conflicts, shared infrastructure limits, or the need to review each agent's patch.
Why two agents in one checkout corrupt each other
An agent does not hold a private snapshot while it works. It repeatedly reads the live tree, runs commands, and writes back. Imagine agent A changing an API response while agent B updates tests. B reads the old response, A runs a formatter, B rewrites the same test fixture, and A stages everything with git add -A. Both agents may report success even though neither reviewed the combined state.
# Unsafe: both processes mutate /srv/app
codex exec --cd /srv/app "Refactor the billing API" &
codex exec --cd /srv/app "Add billing API tests" &
wait
# Shared state includes more than tracked source files:
git -C /srv/app status --short
git -C /srv/app diff --cached
Git's index is especially easy to overlook. A checkout has one index, so staging is a shared mutation. Package managers may rewrite a lockfile; build systems may clean a common output directory; tests may update snapshots. Even if agents promise to edit different source files, their tools can overlap. A worktree supplies a separate working tree and per-worktree index while sharing the repository's object database.
Create branches and worktrees from a known baseline
Begin in a clean coordinator checkout. Fetching is optional, but the base commit must be explicit and stable. Record it before creating workers so you can prove that every task started from the same state.
repo=/srv/app
run_id=20260901-agent-wave-01
base=$(git -C "$repo" rev-parse HEAD)
git -C "$repo" status --short
printf 'base=%s\n' "$base" >"$repo/.git/agent-wave-$run_id"
git -C "$repo" worktree add \
-b "agent/$run_id-api" \
"/srv/worktrees/$run_id-api" "$base"
git -C "$repo" worktree add \
-b "agent/$run_id-tests" \
"/srv/worktrees/$run_id-tests" "$base"
git -C "$repo" worktree list --porcelain
A useful branch name carries the run, responsibility, and no personal identity: agent/<run>-<scope>. Avoid names such as agent-1 or fix-stuff; they become ambiguous in logs and cleanup scripts. Use -b, not -B, because resetting an existing branch would discard or redirect prior work.
Launch each agent with its worktree as the exact working root. Give it the base commit, branch, allowed files, forbidden files, acceptance criteria, and verification command. A directory alone is not a task boundary.
codex exec --cd "/srv/worktrees/$run_id-api" \
"Edit src/api/** only. Do not edit tests or lockfiles.
Run npm test -- api. Commit the reviewed patch." &
codex exec --cd "/srv/worktrees/$run_id-tests" \
"Edit tests/api/** only. Do not edit src or snapshots.
Run npm test -- api. Commit the reviewed patch." &
wait
Use a file-ownership map before agents start
Worktrees prevent live-file interference, but agents can still produce branches that conflict at merge time. Prevent that with a small ownership map. Assign one writer to each path and name shared files separately. Generated clients, schema files, snapshots, package manifests, migrations, and lockfiles are shared even when the visible task sounds independent.
| Agent | May write | Must not write |
|---|---|---|
| API | src/api/** | tests, schema, lockfile |
| Tests | tests/api/** | src, snapshots, lockfile |
| Integrator | schema, snapshots, lockfile | unrelated product code |
Enforce the map after the agent finishes instead of trusting its summary. Compare changed paths with the declared write set. Stop integration when anything unexpected appears.
git -C "/srv/worktrees/$run_id-api" diff \
--name-only "$base"...HEAD
# Example allow-list check for the API worker
git -C "/srv/worktrees/$run_id-api" diff --name-only "$base"...HEAD |
awk '!/^src\/api\// { print "OUT_OF_SCOPE " $0; bad=1 }
END { exit bad }'
Merge in order of integration risk
Do not merge in the order agents finish. Merge the branch that changes the contract or architecture first, then dependent implementation, then tests and documentation. A database migration, API schema, dependency upgrade, or public type has a larger blast radius than an isolated test or prose correction.
- Review each branch against its original base and ownership map.
- Merge contract and migration changes first.
- Merge core implementation next and run focused checks.
- Rebase dependent branches onto the new integration tip if needed.
- Merge tests, docs, and low-risk leaf changes last.
- Run the full acceptance suite on the combined state.
git switch -c "integrate/$run_id" "$base"
git merge --no-ff "agent/$run_id-api"
npm test -- api
# Rebase only after preserving the worker's original commit.
git -C "/srv/worktrees/$run_id-tests" rebase "integrate/$run_id"
git merge --no-ff "agent/$run_id-tests"
npm test
npm run lint
git diff "$base"...HEAD --check
Resolve conflicts in the integration worktree, not by asking two agents to edit the same conflict markers. The integrator owns the combined behavior and can reject a branch when its assumptions no longer hold. A zero-conflict merge is not proof of compatibility; tests and a combined diff review remain mandatory.
Clean up without deleting evidence
First confirm that each branch is committed, reviewed, and either merged or intentionally rejected. Then remove the linked worktrees with Git. Deleting directories directly can leave stale administrative records. Avoid --force until you have inspected untracked and modified files.
git -C "$repo" worktree list
git -C "/srv/worktrees/$run_id-api" status --short
git -C "/srv/worktrees/$run_id-tests" status --short
git -C "$repo" worktree remove "/srv/worktrees/$run_id-api"
git -C "$repo" worktree remove "/srv/worktrees/$run_id-tests"
# Preview stale metadata before removing it.
git -C "$repo" worktree prune --dry-run --verbose
git -C "$repo" worktree prune --verbose
# Delete merged branches only after verifying containment.
git -C "$repo" branch --merged "integrate/$run_id"
git -C "$repo" branch -d "agent/$run_id-api" "agent/$run_id-tests"
If a worktree lives on storage that may disappear temporarily, lock it with a reason so normal pruning does not mistake it for abandoned metadata. If directories were moved manually, use git worktree repair rather than editing files inside .git.
When not to use worktrees
Worktrees are unnecessary for read-only reviewers, one tiny sequential edit, or tasks that can be completed faster than the coordination overhead. They are also the wrong abstraction when workers require different untrusted dependency environments; use containers or virtual machines for process and operating system isolation. Worktrees share Git objects and usually share credentials, network access, databases, ports, and external services unless you isolate those separately.
Do not parallelize tightly coupled work merely because worktrees make it possible. If every task changes the same schema, central module, lockfile, or generated output, serialize the work. A single agent plus a read-only reviewer is often faster than two writers followed by a difficult reconciliation. Parallelism pays when scopes are independently testable and integration has a clear owner.
The final acceptance rule is simple: the coordinator reviews every branch, verifies allowed paths, integrates by risk, and tests the combined tree. Worktrees remove live checkout races. Good task design removes most merge conflicts. Neither replaces engineering judgment.
Preflight every agent wave
Before launching, prove that the planned branches and paths do not already exist. Check disk space as well: linked worktrees share Git objects, but dependency directories and build outputs can still be duplicated. Confirm that each verification command is non-interactive and that workers will not compete for one development port, test database, emulator, or mutable cloud environment. Filesystem isolation does not isolate those resources.
git -C "$repo" status --short
git -C "$repo" show-ref --verify \
"refs/heads/agent/$run_id-api" && exit 73 || true
test ! -e "/srv/worktrees/$run_id-api" || exit 73
df -h /srv/worktrees
git -C "$repo" worktree list --porcelain
# Assign distinct runtime resources when tests need them.
API_PORT=4311 TEST_DATABASE=agent_wave_api \
npm test -- api
Record a compact task packet beside the run evidence. It should name the exact base commit, branch, worktree path, goal, write allow-list, explicit exclusions, verification command, and stop condition. The stop condition matters: an agent that discovers a required edit outside its ownership should report the dependency, not quietly broaden its patch.
task_id: wave-01-api
base: 4a1f3c2
branch: agent/20260901-agent-wave-01-api
worktree: /srv/worktrees/20260901-agent-wave-01-api
writes: [src/api/**]
excludes: [package-lock.json, schema/**, tests/**]
verify: npm test -- api
stop: any required change outside writes
After launch, preserve per-agent logs and final statuses outside disposable build directories. A committed branch proves only what Git recorded; it does not preserve why the agent stopped, which command failed, or whether a generated file was deliberately excluded. The task packet plus raw diff and verification result gives the integrator enough context to accept, revise, or reject the branch without rerunning the entire agent conversation.