Worktrees look simple until the second agent starts a dev server. This lesson is the pile of things that go wrong in practice, then the discipline for merging and cleaning up without losing work.
Things that will bite you
- The base isn’t what you think:
isolation: worktree(the setting that gives a subagent its own worktree) creates the worktree from your repository’s default branch, not from the commit your session is on. Uncommitted work never travels, and neither do commits you haven’t pushed to that default branch. The fix is to commit the foundation and setworktree.baseRefto"head"in your Claude Code settings. That setting takesfresh(the default: start from the remote default branch) orhead(start from your current commit). - Agent teams ignore
isolation: Agent teams (subagents that can message each other) ignore the setting, so don’t assume your teammates are isolated from each other. - A fresh checkout isn’t a ready environment: Ignored files don’t come along for the ride. Install dependencies per worktree. Use
.worktreeincludefor the ones you actually need. It’s a file at the repository root, with ignore-style patterns, that tells Claude Code which ignored files (like.env) to copy into worktrees it creates. It isn’t a Git feature. Generated code, allowlisted.envfiles, Git LFS content (large files that Git stores outside its normal objects), and the instruction files (CLAUDE.mdorAGENTS.md) all need attention too. Run the project’s own check before anyone edits anything. - Passing against the wrong server: Playwright (a browser-testing tool) has a
reuseExistingServeroption that will happily run your tests against the dev server from a different worktree. Cookies aren’t scoped by port. Docker Compose project names change between worktrees, but fixed host ports don’t. Write a negative isolation test: a marker created in worktree A must be invisible from worktree B. - Copying
.envpoints every worktree at the same database. - Expensive shared resources need a lock: Serialize the big build behind a lock instead of letting five agents fight over it.
- Freeze the base: Fetch, record the base commit’s full SHA (its complete hash), create the worktree, then confirm that
HEADmatches that SHA and the status is empty. Now you know every task started from the same place. - Share the download cache, not
node_modules: Install per worktree. Symlinkingnode_modulesacross worktrees is asking for trouble. - Give each task its own resources: A port, a database, a Compose project name, a queue prefix, and a browser context.
Integrate one at a time
For each agent that writes code, record the path, the branch, the exact starting commit, what it owns, its checks, and its allocated ports and database. Then integrate serially: merge one, re-run the full suite, merge the next.
Branches that pass alone can fail together. And if two workers produced competing solutions, pick one. Don’t blend them.
Cleanup is an ownership decision
Deleting a worktree feels like housekeeping. It’s actually a judgment about whether anyone still needs what’s in there.
- Age is not evidence of abandonment.
git rev-list --count HEAD --not --remotestells you whether a worktree has commits that exist nowhere else. - Dirty and unpushed are terminal verdicts. Don’t delete.
- A clean
git statusisn’t safe either.git worktree removewill delete ignored files, like a local SQLite database you kept in the worktree, without asking. git merge-base --is-ancestorwill lie to you after a squash merge. A squash merge writes a new commit, so the original commits never become ancestors ofmain.- Let the model classify. Let reviewable, dry-run-by-default shell code delete. The model is good at sorting worktrees into “obviously done” and “ask a human.” A script you’ve read is better at the
rm.
The safe order of operations:
- Stop the task’s processes.
- Check the status, including ignored files.
- Confirm committed work landed (watch out for squash merges), or preserve its refs and objects with a push or
git bundle. A bundle does not contain ignored or untracked working files. - Copy or archive every needed ignored and untracked file to a location outside the worktree, then verify the preserved contents. Stop database writers first and use a consistent database backup. Do not remove the worktree until that separate preservation is verified.
- Run
git worktree removewithout--force. - Run
git branch -das a separate step. - Optionally, run
git worktree prune.
And here are a few ways to make a mess:
rm -rfinstead ofgit worktree remove.- Renaming a worktree folder outside of Git.
- Leaving work on a detached
HEAD“for later.” - Using
--forcein scheduled jobs.
The thread running through all of them: mistaking the directory for the boundary.
Every worktree you create is one you own, so remove only the ones you made, and only when you can prove nothing is lost.