ambient worktrees
In a perfect world, every change made by an autonomous coding agent is tracked remotely and isolated from the work of other agents.
Even with ‘regular’ Claude Code, making changes from a couple terminal tabs on a single project gets hectic. I found myself making smorgasbord commits like ‘recent stuff’ containing scattered changes from sessions over the past hour. Once scaled up to 5-10 agents working on separate features, isolation becomes a necessity.
The simplest solution is to give each agent a fresh clone of the repository. This works, but it’s not very efficient, as each clone must duplicate the full .git folder, containing all history, objects, etc.
Git worktrees are the natural fit. Each worktree acts as an additional working directory that’s linked to the main repository. It has its own branch, changes, and working files, but shares all underlying .git data. This means they can be created immediately (no network/copying), and can be updated in sync (calling git fetch in one updates them all).
The main constraint of worktrees is that each branch can only be checked out in one worktree at a time. In practice, each agent gets a fresh branch by default, so it serves as a safety net, preventing accidental collisions between concurrent jobs.
However, even with a system that handles branch / worktree creation and plops each Claude into its own directory, tasks kept going wrong. The problem was that Claude would lose track of where the work should be, and had a tendency to switch to main when wrapping up, e.g.
Agent: Work complete! Let me commit and push.
[cd /path/to/main/project && git status]
Agent: Hm, no changes detected. That's strange - let me re-add the files.
Careful prompting helps, like “you are working in a worktree on branch X at path Y, all commits should happen here.” However, it wasn’t a guarantee, and on long runs that get compacted (sometimes more than once), the instructions were often lost in summarization.
The fix was to make git entirely a system responsibility. Branch and worktree are created before Claude starts. When Claude finishes, it calls my MCP tool create_pr, which:
- Runs local CI (errors return to the session for immediate revision)
- Pushes the branch
- Opens the PR
When the PR merges, the system cleans up the worktree and branch. Claude never needs to know the branch name or worry about where to push. Only commits remain.
This still left a ritual at the end of every run: check status, review diff, add, commit, check oneline. Tokens spent mechanically script-following instead of actual problem-solving.
When changes are not committed until the end of a run, any subagents (spawned mid run, branching from parent) are created on the parent’s initial state instead of current. And on long runs with multiple compactions, the final iteration of the agent sometimes thinks “There’s a lot here, but I just fixed a few bugs,” and commits only what it remembers doing.
I wanted to commit every change immediately, so I replaced the native edit, write, and bash tools with custom MCP versions that look and work the same, but add a required commit_msg field. Since the message is written during the change, the “purpose” is immediately accessible instead of summarized in retrospect. By using the shorthand commit_msg="^", Claude can append a change to the last commit, allowing related files to be easily grouped when appropriate. All history is tracked as it happens instead of the usual roundup at the end. As an added bonus, each change event self-documents with a concise description.
Outside of create_pr, git disappears from the workflow entirely. Every change is tracked, every branch is isolated, and neither Claude nor the user needs to keep track of where.