Last updated on
How I run multiple Claude agents on one repo without them colliding
I run several Claude Code sessions against this repo at the same time. One is drafting a migration, another is cleaning up a component, a third is chasing a bug. It's a genuinely good way to work — until you notice that all three are editing the same files on disk, and the wheels come off.
The reflexive fix is "just open more terminals." That doesn't help. Three terminals in the same directory still share one working tree: one index, one set of files, one branch checked out at a time. Two agents saving the same file is a last-write-wins race. One agent running git checkout to switch branches yanks the ground out from under another mid-edit. A git stash meant to tidy up sweeps in work that wasn't yours. I learned that last one the expensive way.
The primitive that actually fixes it is git worktree. Here's how I use it, and — the part that took real work — how I isolated everything the worktree doesn't.
The collision problem
A git repository is two things: the .git directory, which holds all the history and objects, and a working directory, which is one checkout of one commit that you actually edit. The default setup gives you exactly one working directory. Everything — every branch, every agent, every terminal — takes turns using it.
That's fine for one person doing one thing. Point three autonomous agents at it and the shared state turns hostile:
- Dirty tree. Agent A leaves half-finished edits in the tree. Agent B, working on something unrelated, now sees a repo full of changes it didn't make and can't tell apart from its own.
- Branch-switch whiplash. Agent B runs
git switch fix-thing. Agent A was mid-edit onmain. Git either refuses (if the switch would clobber changes) or, worse, succeeds and leaves A editing files that no longer mean what A thinks they mean. - Stash cross-contamination.
git stash push -- some-fileis whole-file, not per-hunk. If two agents both touchedsome-file, stashing "my" changes takes theirs too.
"Just be careful" is not a strategy when the writers are parallel and you're not watching each keystroke. You need real isolation, not discipline.
What a worktree actually is
git worktree add gives one repository more than one working directory. Each is a separate checkout on its own branch, its own files on disk, its own index — all backed by the same shared .git. No re-clone, no duplicated history:
git worktree add ../feature-x -b feature-x
git worktree add ../bugfix-y -b bugfix-yNow feature-x and bugfix-y are two directories you can cd into independently. Edit one, the other doesn't flinch. Commit in one, the other's branch is untouched. It's the isolation of a second clone without the cost of a second clone — the objects are shared, only the checked-out files are duplicated.
This is categorically different from the two things people reach for instead:
- Switching branches in place shares the one working directory, so it serializes everyone. That's the collision problem.
- A full clone isolates, but re-downloads all the history and gives you a disconnected repo you have to push between. Worktrees share the
.git, so a branch committed in one is instantly visible to all of them.
In this repo I keep worktrees under .claude/worktrees/, one per task, named after the issue:
.claude/worktrees/issue-53-worktrees-post → branch for issue #53
.claude/worktrees/issue-64-admin-login → branch for issue #64
.claude/worktrees/issue-73-blob-guard → branch for issue #73Each Claude session lives inside its own directory and is fully insulated from the others. Whatever conflicts exist surface exactly once, at merge time, in the pull request — reviewable and explicit, instead of as a silent mid-edit corruption.
The parallel workflow
The loop is the same for every task:
- Create a worktree from a clean base, on a new branch.
- Run a Claude session inside it. It edits, commits, opens a PR — all without touching any other tree.
- Review the PR, merge it.
- Remove the worktree.
A concrete trio from last week: a feature in one tree, a bugfix in the second, a refactor in the third. Three agents, three branches, three PRs, all in flight. None of them could see or step on each other's uncommitted work, because there was no shared uncommitted work — each had its own tree. Merge order was whatever finished and passed CI first. The one that lost the merge race just rebased and went again.
The single discipline the whole scheme depends on: enter the worktree before the first edit. Not after. The moment you start editing in the shared checkout "just to get going," you've reintroduced the shared mutable state worktrees exist to remove, and now you have to migrate dirty changes out — which is precisely the maneuver that went wrong for me with git stash.
The sharp edges
Worktrees isolate the filesystem checkout. They do not isolate anything else your app touches. This is the part that actually needs saying, because it's where parallel work quietly breaks.
The dev server port. You cannot run next dev in three trees at once — they all want localhost:3000, and the second one either errors or silently grabs 3001, which just leaves you unsure which tree you're looking at. In practice I run the dev server in one tree at a time. The port is shared; the filesystem checkout isn't.
The data layer doesn't come with it. This is the one that bit me hardest, because the checkouts look independent, so you assume everything downstream is too. It isn't. A git worktree still reads whatever .env names — the same database and the same Vercel Blob store every other tree names. A "local" delete in one tree is a real delete in a store the others, and production, can see. The filesystem is forked; the data layer, by default, is not. Worktrees hand you that isolation for exactly nothing, so you have to build it yourself. Which I eventually did — the next section is how.
node_modules and install cost. A fresh worktree has no node_modules, and re-running the install in every tree is slow and wasteful. My worktree tooling symlinks node_modules from the main checkout into each new tree, so creating one is nearly free and no install is needed. That's a deliberate config choice, not a default — vanilla git worktree add gives you an empty tree that pnpm install has to fill. If you adopt this pattern, decide up front whether you're symlinking one shared node_modules (cheap, but every tree runs the same dependency versions) or installing per tree (isolated, but slow and disk-hungry).
Stale worktrees pile up. Every tree you forget to remove is a branch and a directory sitting around. git worktree remove cleans one up; git worktree prune clears out references to trees whose directories are already gone. I wrap this in a small script that removes worktrees whose PRs are merged — and it has to check merge state against the PR, not git branch --merged, because a squash merge flattens your commits into a new SHA that graph-reachability checks read as "not merged."
Isolating the data layer
Closing that gap took two changes, landed a few days apart, and it's the part of this setup I'm happiest with.
The blob store came first, and the fix was to make production unreachable from dev by default. Local development now ignores the production Blob token entirely and writes media to local disk instead. The read path is short-circuited the same way, down to bare HEAD requests, so dev never touches the production CDN in either direction. One explicit opt-in, BLOB_ALLOW_LOCAL=true, exists for the rare time I need to reproduce something against a real store — and the documented way to use it is to point at a dedicated dev store, never prod. Safe by default with an escape hatch, instead of a footgun with a warning taped to it.
The database was the harder half, and it's the one that actually removes the hazard. Every worktree now gets its own database, dfadler_cms_<slug>, instead of sharing one local Postgres. A SessionStart hook repoints the worktree's .env at that database the moment a session opens the tree — and only when the connection string is still the default, so it never clobbers one I set on purpose. pnpm worktree:db-setup creates and seeds the database, from production by default; removing the worktree drops it. So a payload migrate or a destructive seed in one tree can't reach another, which is exactly the thing that used to make me nervous running migrations with several sessions live.
The decision I sat with longest was where those databases should live. Previews already get a Neon branch each, so a Neon branch per worktree was the obvious parallel. I went the other way and put every per-worktree database inside the one local Postgres container I already run. Local won on cost — no extra managed branches — on working offline, and on teardown: dropping a database is an instant DROP DATABASE, not an API round-trip. Neon branches stay reserved for previews, where they buy something a local container can't: a real deployed URL to click.
It isn't all buttoned up. The prune script still only recognizes the worktrees I name by hand, so the auto-generated ones can linger, and the cleanup hook nudges rather than removing merged trees on its own. But the expensive gap — a shared data layer that could reach production — is closed. What's left is housekeeping.
The tie-in: the tooling already does this
Once you've built the mental model by hand, you start seeing it automated. Claude Code's subagents can each run in their own throwaway worktree: when several agents mutate files in parallel, each gets an isolated tree so they can't collide, and the tree is auto-removed if the agent didn't change anything. It's the exact pattern above — worktree per concurrent writer, cleaned up after — with the bookkeeping handled for you.
Which is the real reason to understand the manual version. The automation isn't magic; it's git worktree add per agent and git worktree remove at the end. Knowing that tells you exactly what it does and doesn't protect you from. It isolates the checkout. It does not, on its own, isolate your database, your blob store, or your ports — because nothing at the git layer can. Those are yours to wire up, however the worktrees get created.
Takeaway
Worktrees turn "I can only trust one agent at a time" into "I can fan out work and merge the good parts." The trick to running multiple agents on one repo turns out to be not running them on one working directory — give each its own, backed by the shared .git, and the filesystem collisions simply stop happening.
The filesystem was the free part. The blob store and the database fork only because I built the tooling to fork them; the ports I just take turns on. The day I stopped treating that as "be careful" and started treating it as "build the isolation" is the day parallel agents went from a liability to the fastest way I've found to work.
Comments
No comments yet. Be the first to comment.