Five Quiet Failures: A Bare Repo and Worktrees for Parallel Coding Agents

I wanted to run several coding agents, Claude Code and Codex, in one repository at the same time, one task per agent. Two agents in one checkout share one index and one set of files, so they overwrite each other’s edits. The usual fix is git worktrees. This post shows the layout I use, a bare clone with one worktree per branch, and the five failures I hit on the way. None of the five gave an error. Each one looked like normal work until I found the wrong result.

The layout

Clone the repo bare into a hidden .bare/ folder inside a container directory. Each branch, the default branch too, gets its own worktree beside it:

my-project/              <-- container; git commands work here too
├── .bare/               <-- the git database, no working files
├── .git                 <-- a file: "gitdir: ./.bare"
├── main/                <-- the default branch; no feature work here
├── me-feat-retry/       <-- one task = one branch = one worktree = one agent
└── me-fix-docs/

All worktrees share one object database, so one git fetch updates all of them. Each worktree has its own index, HEAD, and files. I chose this over N full clones because N clones means N fetches, N copies of the history, and branches that exist in one clone but not in the others.

Failure 1: the bare clone has no origin/*

A bare clone does not set up remote-tracking refs. It copies every remote branch as a local branch, and it writes no fetch refspec. So git fetch updates nothing that you can branch from, and “start from the latest origin/main” is not possible.

Older bare-repo recipes fix this with the refspec +refs/heads/*:refs/heads/*. That is worse. While any worktree has a branch checked out, every fetch fails with “refusing to fetch into branch checked out”, and origin/* never moves. With nothing checked out, the fetch force-overwrites your local branches.

The setup that works:

git clone --bare git@github.com:org/repo.git my-project/.bare
cd my-project
echo "gitdir: ./.bare" > .git

git config remote.origin.fetch "+refs/heads/*:refs/remotes/origin/*"
git fetch origin
git remote set-head origin -a
D=$(git symbolic-ref --short refs/remotes/origin/HEAD); D=${D#origin/}
git for-each-ref --format='%(refname:short)' refs/heads |
  while read -r b; do [ "$b" = "$D" ] || git branch -D "$b"; done

git config push.autoSetupRemote true
git config fetch.prune true

git worktree add "$D" "$D"
git branch --set-upstream-to="origin/$D" "$D"

The loop deletes the local copies of remote branches that the clone made. Without it, you get dozens of local branches that never update. push.autoSetupRemote removes the need for -u origin <branch> on every first push. fetch.prune removes origin/* refs for branches that GitHub deleted.

Failure 2: new work starts from an old commit

I use worktrunk (wt) to create, switch between, and remove worktrees by branch name. This config puts each worktree beside main/, with / in the branch name changed to -:

# ~/.config/worktrunk/config.toml
worktree-path = "/../"

wt switch --create <branch> branches from the local main branch by default. A git fetch moves origin/main, not main. So every new task starts from the commit that main/ had the last time you pulled it, and nothing warns you.

The fix is to always pass the base:

git fetch origin
wt switch --create me/feat-retry --base origin/main

I wrap that in a shell function that also starts the agent in the new worktree:

wsc() { git fetch -q origin && wt switch --create "$1" --base origin/main -x claude -- "${@:2}"; }

wsc me/feat-retry 'Implement the retry change described in ...'

The -x flag takes any command. For Codex:

git fetch origin
wt switch --create me/feat-retry --base origin/main -x codex -- 'Implement the retry change ...'

The layout does not depend on the agent. Only Failure 5 below is specific to Claude Code.

Failure 3: the build compiles the wrong worktree

A new worktree is a clean checkout, with no build output. worktrunk has wt step copy-ignored to copy gitignored files, such as target/, from main/ into the new worktree. For an sbt project, that is a trap.

project/target/active.json records which sbt server the sbtn client connects to. A copied active.json sends the new worktree’s sbtn to the server in main/. That server compiles and tests the code in main/, not the agent’s changes. The output looks like a normal build.

sbt 2 also makes the copy unnecessary. It keeps a compile cache in ~/.cache/sbt that all worktrees share. In one run of each on an sbt 2.0.12 project, the first Test/compile in a fresh worktree, server start included, took 10.4 s without the copy and 9.0 s with it.

For other build tools, a .worktreeinclude file (gitignore syntax) limits what gets copied. Leave out anything that records an absolute path or a running process.

Failure 4: one agent pops another agent’s stash

Worktrees do not isolate everything. All state under .bare/ is common to all worktrees:

  • The stash. A git stash in one worktree shows in git stash list in every worktree. An agent that runs git stash pop takes the top entry, which can belong to a different agent. I tell agents to make a WIP commit. The other option is git stash push -m <tag> and then an apply of that entry by SHA.
  • Config and hooks. A git config without --global, in any worktree, changes .bare/config for all of them. The same is true for .bare/hooks/.
  • Refs. Branches and tags are global. Git does refuse to delete a branch that another worktree has checked out.

Failure 5: Claude Code forgets half of what it learned

This failure applies to Claude Code only. Codex does not use autoMemoryDirectory, and the rest of the layout works for it without this fix.

Claude Code keeps state under ~/.claude/projects/<path-with-dashes>/. In this layout that state splits in two:

Session started from Transcripts Auto memory
container root …-my-project/ …-my-project/memory/
any worktree …-my-project-<worktree>/ …-my-project--bare/memory/

A session started at the root sees only the root memories. A session started in a worktree sees only the .bare memories. A symlink from one memory/ directory to the other does not fix this. The index loads, but every read and write of a memory file asks for permission, because the file resolves outside the session’s own memory directory.

The fix is the autoMemoryDirectory setting, set to one directory, in each worktree’s .claude/settings.local.json:

mkdir -p main/.claude
printf '{\n  "autoMemoryDirectory": "%s"\n}\n' \
  ~/.claude/projects/-path-to-my-project/memory > main/.claude/settings.local.json

A worktrunk hook copies that file into each new worktree before the agent starts:

# ~/.config/worktrunk/config.toml
[pre-start]
claude-settings = "if [ -f '/.claude/settings.local.json' ]; then mkdir -p .claude && cp '/.claude/settings.local.json' .claude/; fi"

Keep settings.local.json gitignored. A global **/.claude/settings.local.json line in ~/.config/git/ignore covers every repo. The permissions you approve in main/ go to each new worktree in the same file.

One loud failure: moving the container

This one does give an error. Git links each worktree to .bare/ with absolute paths, in both directions. If you move or rename the container, every worktree fails with fatal: gitfile does not point to a valid repository. Run this from the new path:

git worktree repair */

Do not run git worktree prune before the repair. It deletes the records of the broken worktrees.

Rules I give the agents

  • One agent per worktree, and start each session inside its worktree. Then relative paths, build output, and the build server all belong to that task.
  • Create worktrees only with wt. Do not use claude -w or codex --worktree in this layout. claude -w puts its worktrees under .bare/.claude/worktrees/ on branches named worktree-<name>, and mixed tools scatter worktrees across the disk.
  • Commit the shared repository rules in AGENTS.md, which is not specific to Codex. Keep Claude Code settings in .claude/.
  • No feature work in main/. It is the reference copy that agents read and diff against.
  • Each worktree runs its own sbt server, so N active worktrees means N JVMs. Run sbtn shutdown when a task goes idle.

Cleanup

git fetch origin
wt remove me/feat-retry      # refuses if dirty; deletes the branch only if it is merged
wt remove -D me/feat-retry   # also deletes an unmerged branch
git worktree prune           # forgets worktrees whose directories you deleted by hand

wt remove recognizes squash merges. In my test it deleted a squash-merged branch after other commits landed on origin/main, and it kept a branch with unmerged changes. It compares against origin/main, so fetch first.

What I would check first

If you already have a bare-repo setup from an older recipe, check these four things:

git config --get-all remote.origin.fetch   # want +refs/heads/*:refs/remotes/origin/*
git for-each-ref refs/heads | wc -l        # dozens = copied branches never cleaned up
git worktree list                          # 0000000 = the worktree's branch is gone
ls ~/.claude/projects | grep my-project    # two entries = split memory
Finding Fix
refspec +refs/heads/*:refs/heads/*, or no refspec Set +refs/heads/*:refs/remotes/origin/*, then git fetch
local branches that match their origin/* copy Compare the SHAs, then delete the local branches
a worktree that shows 0000000 Compare its index with the merged PR head, then git worktree remove --force
two .claude/projects entries Merge them into one memory/ directory and set autoMemoryDirectory

Before you delete anything, save .bare/config and the output of git for-each-ref. If you want to keep a local-only branch, make it a tag: git tag -a archive/<name> <branch>.

The result: several agents work in one repo at the same time. Each one starts from the latest origin/main, builds its own code, and shares one memory with the others.

Author | Joe O'Pecko

Software engineer