Why NextWhy Next
Back to posts
💻 Dev10 min read

What Happened When I Let Several AI Agents Loose in One Repo

I thought running agent sessions in parallel would multiply productivity. What I actually got, in the space of one week, was a string of real incidents: hijacked branches, commits landing on someone else's branch, orphaned commits rescued from the reflog, and the same feature implemented twice. A typology of the accidents parallel agents cause when they share one checkout, and the rules I built after living through them.

TL;DR

The real enemy of parallel agent sessions isn't model skill but the shared working directory. HEAD is everyone's global variable, so one session switching branches lands another session's commit in the wrong place. Four accident types from one week and the recoveries, plus three lines of defense: worktree isolation, re-checking the branch right before committing, and narrowing the verification gate's scope.

On this page

Work with AI agents for a while and the ambition comes naturally. While one session fixes a bug, another can refactor, and a third can investigate an issue, right? You can spin up as many models as you like, so productivity should scale to match.

That's how I started too. And within a week I learned that the real enemy of parallel agents isn't the models' skill. It's the working directory they share.

HEAD is a global variable

The cause fits in one sentence. When multiple sessions share a single git checkout, the current branch becomes everyone's global variable.

Picture two people working on one computer at the same time and the absurdity is obvious, but that thought never occurred to me while spinning up agents. With one session per terminal tab, they look isolated from each other. But there is one filesystem, and one HEAD. The moment one session runs git checkout, the ground shifts under every other session.

The incidents from that week fell into clear types.

Branch hijacking. While session A was working on a topic branch, session B switched branches to do its own work. A committed without knowing, and the commit landed on top of B's branch. It happened in the other direction too: right as A was about to commit, the branch had been switched to develop, and only the hook that blocks direct commits to protected branches saved it. Without the hook, it would have gone straight in.

Orphaned commits. Session B deleted session A's topic branch during a cleanup pass. A's commits became orphans belonging to no branch, and I dug through the reflog, found the commit hashes, and recovered them with cherry-pick. Lucky that it worked; if the reflog had expired or I hadn't found them, the work would have simply evaporated.

Staging contamination. At the moment session A was creating a commit, a file deletion that session B had staged was sitting in the staging area alongside it. Committed as-is, B's deletion would have been folded into A's commit. It only got filtered out because an unfamiliar change showed up while skimming the diff. If the agent hadn't looked at the diff right before committing, nobody would have known.

Duplicate implementation. The most deflating type. Two sessions, each unaware of the other, implemented the same feature independently. Both did a diligent, solid job, and one had to be thrown away wholesale. The time saved by parallelizing was handed straight back.

They even trip each other's verification gates

Files and branches weren't the only problem. Our harness has a gate that runs the repo-wide static analysis and tests before a session can finish, so work can't end in a broken state. For a solo session, it's an excellent mechanism.

With parallel sessions, it became a trap where they trip each other. Session A only touched documentation, but A's gate fails because of a compile error in a file session B is midway through fixing. A burns turns proving "this isn't my change," and on a bad day waited 30 minutes for B to clean up. The worst incident was a session modifying code that another session was working on, just to get its own gate to pass. The gate itself became an incentive to touch someone else's work.

The check isn't what's wrong. The check's scope being "the whole repo" became wrong the moment the repo stopped belonging to one person.

Three lines of defense

After going through the accidents type by type, I put up three layers of defense.

  1. Worktree isolation as the default. Give each session its own independent working directory with git worktree and HEAD is no longer a shared variable. Branch hijacking, orphaned commits, and staging contamination disappear at the root. Three of the four accident types above vanish with this one move. It isn't free, though. In a monorepo, every worktree needs its own dependency install and code generation, and in a repo using tools that get along badly with worktrees, like git-crypt, it's hard to enforce. We keep one repo on a shared checkout because of exactly that constraint, which is why the other two defenses exist.
  2. Re-check the branch right before committing. When a session starts work, it records "I am working on this branch," and right before committing it compares that against the current HEAD. If they differ, stop the commit and figure out what happened first. The rule is almost embarrassingly simple, but every branch-hijack incident boiled down to "the HEAD at commit time wasn't the HEAD I knew about." A human would notice the branch name sitting in their prompt; an agent doesn't check unless explicitly told to.
  3. Scope the verification gate to my changes. Narrow what the session-exit gate inspects, from the whole repo down to the files that session actually modified. No more getting my exit blocked by someone else's WIP, and no more incentive to touch someone else's code just to get the gate to pass. The health of the whole repo is something CI re-checks against committed state anyway. The session gate never needed to look at everything.

On top of these, one operating rule. Before spinning up a session, skim the currently open branches and PRs and check for overlapping scope. Duplicate implementation isn't a problem git can block; it's a dispatching problem, so the only way to prevent it was habit, not tooling.

Concurrency problems were never just a database thing

Laying it all out, the picture looks familiar. A shared resource, lock-free concurrent access, race conditions, and isolation levels. The exact problem we spent decades learning about in databases and multithreaded code, replayed on top of a working directory.

With a single agent, the problem doesn't exist. Even when a human and an agent take turns, the human implicitly plays coordinator. The trouble starts the moment there are multiple agents and the human lets go of the coordination. From that point on, the working directory is a shared resource that needs concurrency control, and if you run it without isolation, you will lose data, just as databases did.

This is not an argument for giving up on parallel agents. I still spin up multiple sessions every day. Only one thing changed: I no longer cram them all into the same room.

Related posts

0 comments