Parallel Claude Code agents need more than a worktree
Every guide to parallel coding agents gives the same advice. One task, one branch, one worktree. Claude Code builds it in, and its documentation says a second claude --worktree will "start an isolated parallel session".
A worktree gives each agent its own files. It does not give it its own git. Every worktree of a repository writes to one store of branches and commits, and Claude Code's sandbox lets every agent write to it. So one agent, inside its sandbox, can delete another agent's unpushed work for good. It takes three commands.
Every worktree writes the same .git
Git's documentation says a new worktree is linked to the repository, "sharing everything except per-worktree files such as HEAD, index, etc." Its files sit in a folder of their own. Its branches and its objects live in the main .git, beside yours.
Claude Code's documentation says the same of its worktrees. Git commands in a worktree "write to the main repository's shared .git directory". For git commit to work, the sandbox has to let the agent write there.
What Claude Code's sandbox already protects
The worst door, already. In a linked worktree, the sandbox "allows writes to the main repository's shared .git directory so commands such as git commit can update refs and the index. Writes to hooks/ and config inside that directory remain denied."
That stops a hook the agent writes from running later in your own shell. We tried it on Claude Code 2.1.280 on macOS. Writing to .git/hooks fails with operation not permitted. So does rewriting the worktree's commondir, the small file that tells git where the shared folder is.
What it leaves open
The refs and the objects, because a commit writes them. Take two worktrees, agent-1 and agent-2, each with its own agent. agent-2 holds a commit it has not pushed. From inside agent-1, in the sandbox, all three of these succeed.
git update-ref -d refs/heads/agent-2
git reflog expire --expire=now --all
git gc --prune=now
The first deletes the other agent's branch, which git branch -D refuses to do while another worktree has it checked out. The second and the third remove its commit from the disk. agent-2 is left on a branch with no commits. Its files are still in its folder, but the commits, their messages and their history are gone.
git update-ref refs/heads/main HEAD succeeds too. It moves the main your own checkout is on, and your next git status shows changes you never made.
No agent has to mean harm for this. An agent asked to tidy old branches or free some disk space may run these very commands.
A repository of its own for each agent
Stop sharing the .git. Give each agent a repository whose git data is its own, and let it borrow the clone's history instead of copying it. Run this from your clone.
git init --template= --separate-git-dir=../agent-1.git-data ../agent-1
mkdir -p ../agent-1.git-data/objects/info
echo "$PWD/.git/objects" > ../agent-1.git-data/objects/info/alternates
git -C ../agent-1 fetch "$PWD" "refs/remotes/origin/*:refs/remotes/origin/*"
git -C ../agent-1 checkout -b agent-1 origin/main
--template= leaves out the sample hooks. The alternates file lets the new repository read the clone's objects, so its own objects folder starts almost empty. Then start the agent in agent-1, with the sandbox's writes narrowed to what is its own.
"filesystem": {
"allowWrite": ["/work/agent-1.git-data"],
"denyWrite": [
"/work/agent-1.git-data/config",
"/work/agent-1.git-data/hooks",
"/work/agent-1.git-data/commondir",
"/work/agent-1/.git",
"/work/clone"
]
}
Replace /work with the folder that holds your clone. The sandbox already lets the agent write in agent-1, the folder it starts in. These settings add its git data, so it can commit. They deny, by their literal paths, the files that decide what git runs. These are the config and the hooks of its git data, a commondir it could add there, and the .git file that points its files at that data. The clone is denied whole, so update-ref on its branches fails, and a gc in the agent's repository prunes only its own objects. We ran each of those writes in the sandbox, and each one failed.
If your settings open a wider folder for writes, as the home is in our earlier settings, deny every other agent's folder and git data whole.
Push from outside the sandbox
With a repository of its own, the agent has no reason to reach your code host. Deny your code host's domains in the sandbox's deniedDomains, as the third trap of Claude Code's sandbox shows, and let a process outside the sandbox push the agent's branch from the clone, borrowing the agent's objects.
GIT_ALTERNATE_OBJECT_DIRECTORIES=/work/agent-1.git-data/objects \
git -C /work/clone -c core.hooksPath=/dev/null push --no-verify origin \
"$(git -C /work/agent-1 rev-parse HEAD)":refs/heads/agent-1
One branch, never forced, and no hook runs. If the branch moved on the code host meanwhile, the host refuses the push.
How we lay it out
Tillerly runs AI coding agents on your team's machines, sandboxed, from ticket to pull request. Its Runner sets up the layout above for every ticket. Tillerly still calls it the ticket's worktree, the name its tools use, but it is not a git worktree.
- Each ticket gets a repository of its own, in
<repository>.tickets/slot-<n>/<ticket>, its git data beside it and outside any.git, borrowing the clone's objects. - The Runner makes it in a folder the session cannot write, then moves it into place, so no agent touches it while it is being made.
- The agent reaches it only through four tools the Runner serves on your machine,
open_worktree,sync_worktree,push_branchandremove_worktree. - Every other agent's folder is denied whole, and so is the clone.
- The Runner pushes the ticket's branch from the clone, never by force, with every hook pointed at nothing. Your team opens the pull request.
How the Runner keeps worktrees, and what the sandbox sets for every session.
What it costs
- A checkout for each agent. The history is borrowed, the files are not, so each agent's folder takes the size of a checkout on your disk.
- The clone must keep what it lends. A
git gc --prunein the clone, after a branch is deleted there, can remove objects an agent's repository still borrows. Tillerly's Runner turns off git's automaticgcin every command it runs. - The repository's own hooks do not run in the agent's session. A check your
pre-commithook makes has to be one the agent runs itself, and your CI still checks every push. - A folder may hold work nobody pushed. Remove it only once its pull request is merged or declined. Tillerly removes a ticket's folder then, and never on its own otherwise.
Your team still opens every pull request and merges it. The difference is that one agent's cleanup can no longer reach another agent's work.