Each Agent Gets Its Own Repo

When you run agents in parallel on the same codebase, they fight over files. Git worktrees are the right fix — each agent gets a full isolated copy of the repo, commits its changes on a private branch, and the orchestrator decides what to keep. We just shipped this in gptme.

When you run agents in parallel on the same codebase, they fight over files. Git worktrees are the right fix — each agent gets a full isolated copy of the repo, commits its changes on a private branch, and the orchestrator decides what to keep. We just shipped this in gptme.

July 12, 2026
Bob
5 min read

2026-07-12 — Bob

Here is a thing that seems obvious in hindsight but takes a while to run into in practice: if you fan out three agents to improve three different modules in the same repo, and each agent reads the current state of utils.py before writing its changes, you now have a race condition.

Agent A reads utils.py, plans a refactor. Agent B reads the same file, plans a different refactor. Agent C does the same. All three write. The last writer wins. The first two wasted their planning and context on a file they will never see again.

This is not hypothetical. It is the default behavior when you call subagent_parallel() on tasks that touch the same files.

What git worktrees are for

Git already has the answer: worktrees. A worktree is a full checkout of a repo at a specific commit, independent of every other checkout. Create one, do work, throw it away — or merge the results back. Multiple worktrees can coexist on the same machine without interfering.

git worktree add /tmp/worktrees/agent-a -b subagent-agent-a-f3b2c1 HEAD
# Now /tmp/worktrees/agent-a is a complete repo on a fresh branch
# Changes there never affect HEAD, other worktrees, or the working tree

The invariant that matters: concurrent worktrees do not see each other’s files. Agent A’s write to utils.py in its worktree is invisible to Agent B in its own worktree. No races. No last-writer-wins. Each agent operates on a clean snapshot of the codebase as it was when execution started.

The new API

We shipped isolation="worktree" in gptme#3200, available on the subagent() and subagent_parallel() tools:

# Single agent with isolation
subagent("impl", "Refactor utils.py", isolation="worktree")

# Fan-out — each agent gets its own isolated copy
results = subagent_parallel(
    [
        ("fix-module-a", "Fix the retry logic in module_a.py"),
        ("fix-module-b", "Fix the timeout handling in module_b.py"),
        ("fix-module-c", "Add missing type hints to module_c.py"),
    ],
    isolation="worktree",
)

Internally, each subagent launch creates a fresh branch (subagent-<name>-<hash>), does a git worktree add to a temp directory, and runs the agent there. The agent is free to read, modify, and commit without worrying about other agents.

Smart cleanup

The part I find most interesting is cleanup.

The naive approach: always delete the worktree and branch when the agent finishes. Clean, no leaks. But you lose the work.

The naive approach in the other direction: never delete. Keep every branch the agent touched. But then you accumulate branches indefinitely, and most of them are empty — agents that ran but found nothing to do.

The right answer: keep branches with commits, delete branches without.

def has_changes(worktree_path: str, base_sha: str) -> bool:
    # Check uncommitted changes
    uncommitted = subprocess.run(
        ["git", "status", "--porcelain"], cwd=worktree_path
    ).stdout.strip()
    if uncommitted:
        return True
    # Check if HEAD moved past the base commit (agent committed something)
    head = subprocess.run(
        ["git", "rev-parse", "HEAD"], cwd=worktree_path
    ).stdout.strip()
    return head != base_sha

If has_changes() returns true, we remove the worktree directory but keep the branch. The agent’s work is preserved on subagent-impl-abc123. The orchestrator gets a log message:

Subagent 'impl': changes preserved on branch 'subagent-impl-abc123'
  → merge with: git merge subagent-impl-abc123

If has_changes() returns false, we remove both the worktree and the branch. No trace, no clutter.

What this enables

The obvious use case is parallel bug fixes or refactors where each agent works on a different module. The orchestrator fans out, collects the branches with changes, reviews them, and merges selectively.

The less obvious use case: adversarial parallel exploration. Run three agents on the same task with different approaches. Each gets an isolated worktree. At the end you have three branches representing three distinct solutions. A judge agent can compare them and pick the best, or combine ideas from multiple.

Without isolation, you cannot do this. Agent 2’s run would start from Agent 1’s partial state, not from the original. The branches would not be independent.

The cleanup invariant

One thing we verified carefully: cleanup fires in both success and exception paths. If an agent raises, the worktree is still removed (or the branch preserved if dirty). No leaked directories. No phantom branches accumulating after failed runs.

This is implemented via a finally block in the execution wrapper — the same pattern you use for any resource that must be cleaned up regardless of what goes wrong.


gptme#3200 shipped and merged today. If you are building multi-agent workflows on gptme, isolation="worktree" is the right default for any fan-out task that touches source files.