●2.1.302 — Fork subagents are now on by default in claude -p, the Agent SDK and the desktop app. Turn them off with CLAUDE_CODE_FORK_SUBAGENT=0 (10/16)●2.1.301 — Unattended retries now stop on their own when a usage limit is hit instead of waiting (10/15). Latest is 2.1.304 (10/19)●SONNET 4.5 — Retires on the Claude API on 11/30, 40 days left. Migrate to Sonnet 5.5●WINDOWS — Reports say hooks break silently only on Japanese-locale Windows; check the character encoding first●FAST MODE — Opus 5.5 fast mode is out of research preview and no longer needs the beta header (10/15)●NEW — I Put a Ten-Line Promise File at the To…●2.1.302 — Fork subagents are now on by default in claude -p, the Agent SDK and the desktop app. Turn them off with CLAUDE_CODE_FORK_SUBAGENT=0 (10/16)●2.1.301 — Unattended retries now stop on their own when a usage limit is hit instead of waiting (10/15). Latest is 2.1.304 (10/19)●SONNET 4.5 — Retires on the Claude API on 11/30, 40 days left. Migrate to Sonnet 5.5●WINDOWS — Reports say hooks break silently only on Japanese-locale Windows; check the character encoding first●FAST MODE — Opus 5.5 fast mode is out of research preview and no longer needs the beta header (10/15)●NEW — I Put a Ten-Line Promise File at the To…
Running Claude Code Per Worktree: Notes From Actual Use
Implementation notes on splitting branches into Git worktrees and launching Claude Code per directory. What is shared and what is isolated, measured on a real repository, plus the untracked files nobody warns you about.
On a Friday evening I got word that a shipped app was crashing for some users. Meanwhile my working tree was half-way through a feature for a different app, in no state to commit. Switch branches and the unfinished work goes into limbo; stay put and I cannot start the fix. That fork in the road is the part of solo development that genuinely makes my stomach tighten.
From that day I started splitting the working tree itself instead. Add one more directory with Git worktrees, launch a second Claude Code session inside it, and treat the change as moving to another desk rather than swapping the contents of the one desk I have.
What follows is a set of notes I wrote while checking each behavior on Git 2.34 in front of me. Knowing what is separated and what quietly stays shared prevents far more accidents than memorizing another list of commands.
What a Worktree Splits, and What It Shares
Adding a worktree gives you another directory. It does not give you another repository. Miss that and you will be surprised when an operation in one worktree shows up in the other.
The .git entry in an added worktree is a text file, not a directory. It holds a pointer back to the parent and nothing else.
$ git worktree add ../wt-a feature/aPreparing worktree (checking out 'feature/a')HEAD is now at dc6dc31 a$ file ../wt-a/.git../wt-a/.git: ASCII text$ cat ../wt-a/.gitgitdir: /home/me/demo/.git/worktrees/wt-a
The fastest way to feel how much is shared is the stash. Something shelved in the main working tree is visible from the added one.
# Shelve in the main working tree$ git stashSaved working directory and index state WIP on main: c88bbda init# List it from the added worktree$ git -C ../wt-a stash liststash@{0}: WIP on main: c88bbda init
Local config behaves the same way. Write it on one side and read it from the other.
One side can be mid-conflict while the other stays calm
Commits, branches, tags
Shared
One object database for all of them
Stash
Shared
Anything shelved is visible everywhere
.git/config (--local)
Shared
Per-worktree values need extensions.worktreeConfig
Hooks and remotes
Shared
A hook you install applies to every worktree
Only the desk in front of you is separate. The repository's memory stays single. Internalize that one line and the "why can the other worktree see my stash" moment never happens.
A New Worktree Does Not Come With a Working Environment
This is where I lost the most time on day one. git worktree add materializes what is committed, and nothing that .gitignore covers.
$ ls -a ../wt-a. .. .git .gitignore srcnode_modules: missing.env.local: missing.claude/settings.local.json: missing
The new worktree is a complete repository and an empty development environment. Dependencies, local environment variables, and your machine-local tool settings all have to be carried over deliberately. My first attempt ran npm run dev right after git worktree add, watched it fail, and spent twenty minutes suspecting Git. The dependencies simply were not installed.
So creating the worktree and provisioning it now live in one script.
#!/usr/bin/env bash# wt-new.sh — create a worktree and carry over what Git will notset -euo pipefailBRANCH="$1" # e.g. hotfix/login-timeoutDIR="../wt-${BRANCH//\//-}"ROOT="$(git rev-parse --show-toplevel)"git fetch -q origin maingit worktree add "$DIR" -b "$BRANCH" origin/main# Copy untracked-but-needed files by name, never in bulkfor f in .env.local .claude/settings.local.json; do [ -f "$ROOT/$f" ] || continue mkdir -p "$DIR/$(dirname "$f")" cp "$ROOT/$f" "$DIR/$f"done( cd "$DIR" && npm ci --prefer-offline )echo "ready: $DIR"
The explicit list matters. Mirroring everything .gitignore hides would happily duplicate production keys and credentials for unrelated apps into a fresh directory. Whatever gets carried across, gets named. It is a small tax I would rather keep paying.
CLAUDE.md is tracked, so it arrives on its own. .claude/settings.local.json is not, so it never does. That asymmetry leads straight into the next section.
✦
Thank you for reading this far.
Continue Reading
What follows includes implementation code, benchmarks, and practical content we hope you'll find useful. This site runs without ads — server and development costs are supported entirely by members like you. If it's been helpful, we'd be truly grateful for your support.
WHAT YOU'LL LEARN
✦A measured breakdown of what worktrees share (stash, config, hooks) and what they isolate (working tree, index, HEAD)
✦Why node_modules, .env.local and .claude/settings.local.json never follow a new worktree, and a script that fills the gap
✦The full path from spinning up a hotfix worktree to tearing it down without leaving prunable entries behind
Secure payment via Stripe · Cancel anytime
✦
Unlock This Article
Get full access to the rest of this article. Buy once, read anytime. This site is ad-free — your support goes directly toward keeping it running.
$ git worktree add ../wt-dup mainfatal: 'main' is already checked out at '/home/me/demo'
Git is not being difficult. It is refusing to let two places move the same branch's HEAD. There are three ways around it:
Create a new branch for the worktree — ordinary feature work
Attach an existing, different branch — pulling a review down locally
Detach from any branch — read-only inspection
# 1. Grow a branch along with the worktreegit worktree add ../wt-hotfix -b hotfix/login-timeout origin/main# 2. Lay out a colleague's branch for reviewgit worktree add ../wt-review review/pr-482# 3. Inspect a tag without consuming a branchgit worktree add --detach ../wt-inspect v2.3.0
That third form earns its keep whenever I need to run shipped code locally to reproduce something. No branch is spent, so tearing it down later carries no hesitation.
Launching Claude Code Per Worktree
Here is the anticlimactic core of it: there is no special flag to learn. Claude Code works against the directory you launch it from, so moving into a worktree and starting it there is the isolation.
# Tab 1: feature work, left untouchedcd ~/projects/wt-feature-dashboardclaude# Tab 2: the interruption, separate branch and directorycd ~/projects/wt-hotfix-login-timeoutclaude
Three things stood out once I had lived with this for a while.
Conversation context accumulates per task — history builds per working directory, so picking up the next morning does not drag in an unrelated thread
Permission memory does not transfer — .claude/settings.local.json is untracked, so a fresh worktree either asks again or gets the file from the script above
CLAUDE.md can be overridden per worktree — a fix-only directory gets a short file saying exactly which paths are in scope
When a session genuinely needs to read another worktree, --add-dir grants that directory explicitly at launch. The flip side is the useful part: without it, nothing reaches sideways. That narrow default is what makes running two sessions feel safe.
One rule I hold firmly: never let two sessions edit the same file. Ownership is split by directory, and when a shared utility has to change for both, one side stops and the changes go in sequence. Parallelism buys back waiting time, not conflict resolution — something I learned properly after rewriting the same function twice and losing half a day to it.
How the Interrupted Friday Goes Now
Replaying that evening with the current setup:
Leave the unfinished work alone. Changing directories is the whole move
Run ./wt-new.sh hotfix/login-timeout to grow a worktree for the fix
Start Claude Code there, hand it the repro conditions and logs, narrow the cause
Test and build inside that worktree only, then push and open the PR
While review is pending, walk back to the original directory and keep building
Once merged, fold the worktree away and delete the branch
# 2. Grow it./wt-new.sh hotfix/login-timeout# 4. Verify inside that worktree alonecd ../wt-hotfix-login-timeoutnpm test && npm run buildgit push -u origin hotfix/login-timeout# 6. Fold it awaycd -git worktree remove ../wt-hotfix-login-timeoutgit branch -d hotfix/login-timeout
Step 5 is where the value actually shows up. The review window is not spent on a third context — it is spent returning to the work that got interrupted.
Cleanup: Leave No Prunable Entries
Deleting a worktree directory by hand leaves the registration behind, and the list drifts away from reality.
That prunable marker is your permission to clean. For a long-running investigation worktree, the opposite is useful — lock it so nothing removes it by accident.
$ git worktree lock ../wt-b --reason "reproducing a crash"$ git worktree remove ../wt-bfatal: cannot remove a locked working tree, lock reason: reproducing a crashuse 'remove -f -f' to override or unlock first
For scripted housekeeping, parse the machine-readable listing rather than the human one.
# Pull out paths and flag anything untouched for two weeksgit worktree list --porcelain \ | awk '/^worktree /{print $2}' \ | while read -r d; do [ -d "$d" ] || continue find "$d" -maxdepth 1 -mtime +14 -name .git -print done
Disk cost turned out smaller than I expected. On the repository I measured, .git totaled 308 KB and the admin files for one worktree came to 32 KB — roughly 10% of it. Unlike an extra clone, objects are never duplicated, so the only real growth is the source files themselves.
How Many Is Too Many
Early on I kept five worktrees open with a session in each. It went badly. Everything advanced halfway, and by the next morning I could not recall which directory was in the middle of what.
Two at a time is my limit now, three at the very most. The point of another worktree is to absorb waiting time, not to think about more things at once.
Add places to work, not places to decide. As long as the decision still happens in one spot, running things side by side stays safe.
When I am unsure, three questions settle it. Can I name what a directory is mid-way through, just from its name? If I stop now, can tomorrow's version of me resume? And is that task really blocked on waiting? A "no" to the third usually means the work belongs in a queue rather than a second worktree.
For the Next Interruption
Set up a single worktree now, before you need it, as the place to land when something urgent arrives. Even git worktree add --detach ../wt-inspect HEAD, which consumes no branch at all, noticeably lowers the friction of switching.
Since I stopped having to choose between unfinished work and a Friday-evening bug report, the weekends have felt considerably lighter. Thank you for reading.
Share
Thank You for Reading
Claude Lab is ad-free, supported entirely by members like you. We publish practical guides daily with implementation code, benchmarks, and production-ready patterns. If you've found it useful, we'd love to have you on board.