Claude Code Worktree Tooling
Scope: EnterWorktree, ExitWorktree, worktree.baseRef, worktree.bgIsolation settings, the background-session isolation guard, and the squash-merge sync protocol
Load if: The bg-isolation guard refused an edit, OR EnterWorktree failed, OR the agent is planning a multi-file change that warrants isolation, OR the user mentions worktrees / bgIsolation / baseRef, OR cleaning up after a worktree-based PR merge
Prerequisites: @smith-git/SKILL.md (git worktree fundamentals), @smith-gh-pr/SKILL.md (PR flow context)
Authoritative sources: https://code.claude.com/docs/en/changelog (worktree.baseRef v2.1.133, bgIsolation v2.1.143; verified 2026-05-21); https://code.claude.com/docs/en/worktrees "How Claude Code enforces isolation" (verified 2026-09-07 on v2.1.263)
CRITICAL: Worktree Discipline
- Use
EnterWorktree/ExitWorktreeinstead of raw Bashgit worktree add/git worktree remove— the Claude Code tools track per-session ownership; raw Bash leaves orphans the harness can't clean up. - Keep the bg-isolation guard enabled rather than disabling it transiently for "one edit" — the guard exists because parallel background jobs share the working copy and clobber each other.
- Scope
worktree.bgIsolation: "none"to the repo's.claude/settings.json(not the user-global level~/.claude/settings.json) when the user has indicated this repo prefers in-place edits. - After
gh pr merge --delete-branchfrom inside a worktree session, sync local main manually (see Sync-After-Squash-Merge below). On gh older than 2.99.0 the command exits non-zero withfatal: 'main' is already used by worktree at ...even though the merge itself succeeded; gh 2.99.0+ skips the local cleanup with a warning instead (cli/cli#14007, fixes cli/cli#3442). Always confirm withgh pr view «n» --json staterather than trusting the exit code. - Every
gitcommand inside a worktree session passes Claude Code's built-in isolation guard (see Built-in Isolation Guard below). Run git plainly — no launcher in front of it, nogit -C «primary-checkout», no chainedcd. - For multi-file edits in a background session, the bg-isolation guard will refuse the first
Editand tell the agent toEnterWorktree. Comply on the first refusal — do not try alternative edit paths. - After a squash-merge of a worktree branch, the local copy of that feature branch is an orphan (its commit is not in main's history under the same SHA).
git branch -dwill refuse it with "not fully merged"; usegit branch -D(force) once the squash commit is confirmed on main. - Branch naming:
EnterWorktreeauto-names the branchworktree-«name»which violates@smith-style/SKILL.md. MUST rename before pushing:git branch -m «type»/«description»Alternative: create branch first, thenEnterWorktree({path: ...})
EnterWorktree Semantics
EnterWorktree({name: "«n»"}) creates .claude/worktrees/«n»/ on local branch worktree-«n», branched from the ref specified by worktree.baseRef. The session's CWD switches into the worktree.
A new worktree starts with a CLEAN tree: uncommitted changes in the current
checkout never carry over — they stay behind, stranded from the task. The
worktree-dirty-guard PreToolUse hook
(smith-git/scripts/hooks/worktree-dirty-guard.mjs, registered
user-globally) blocks EnterWorktree while git status --porcelain is
non-empty; resolve deliberately (commit, stash-and-apply inside the worktree,
or branch in place) before retrying.
-
Naming: the
nameparam accepts/-separated segments; each segment may contain letters, digits, dots, underscores, dashes only. The branch name is alwaysworktree-«name»regardless. -
Base ref (
worktree.baseRefsetting in~/.claude/settings.jsonor repo.claude/settings.json):fresh(default since v2.1.133) — branches fromorigin/«default-branch». Ignores local unpushed commits on main.head— branches from localHEAD. Use when iterating on top of work that isn't on origin yet.ExitWorktree({action: "keep"|"remove", discard_changes: bool}):
-
keep— leaves the dir + branch on disk; session CWD restores to the original. Use when the work isn't finished or shouldn't be discarded. -
remove— deletes both. Refuses if uncommitted files or commits exist on the branch unlessdiscard_changes: true. -
Operates only on worktrees this session created via
EnterWorktree. A worktree created manually withgit worktree addis unaffected; useEnterWorktree({path: ...})to switch into it instead.
Gitignored Local Skills and .worktreeinclude
A worktree holds tracked files only. Gitignored personal files in the
primary checkout — .claude/skills/*-local/ skills, CLAUDE.local.md —
are absent.
- Since v2.1.277 a worktree with no
.claude/skillsdirectory reads the main checkout's project skills. A repo that tracks any skill gives every worktree its own.claude/skills, and then "only that copy loads" — the gitignored local skills silently disappear from the skill list. - Fix: list them in a
.worktreeincludeat the primary checkout root (.gitignoresyntax; only paths that match AND are gitignored are copied):
The docs apply it to every worktree Claude Code creates with git (.claude/skills/*-local/** CLAUDE.local.md--worktree, subagent, desktop parallel sessions); tested 2026-09-24 on v2.1.281 withEnterWorktreeandclaude --worktree. - Gitignore
.worktreeincludeitself (global gitignore) — an untracked one makes the checkout dirty, andworktree-dirty-guardthen blocks everyEnterWorktree. - Copies, not symlinks: edit local skills in the primary checkout; changes made to a worktree copy are lost when the worktree is removed.
- Directory symlinks are NOT copied (probed 2026-09-26, v2.1.282), so
.worktreeincludecannot share.serena/memories; thelink-worktree-memoriesSessionStart hook does that instead (smith-serena/references/HOOKS.md). - A
WorktreeCreatehook replaces default creation and skips.worktreeinclude; copy the files in the hook script instead. CLAUDE.local.mdalternative that needs no copy: import a home-directory file,@~/.claude/«project»-instructions.md. It is an external import: the first one shows an approval dialog, and declining disables it for the project without asking again, so the instructions silently stop loading.
Sources (retrieved 2026-09-24): https://code.claude.com/docs/en/worktrees,
https://code.claude.com/docs/en/hooks, https://code.claude.com/docs/en/memory;
a dedicated local-skills directory was declined
(https://github.com/anthropics/claude-code/issues/81110), and copying
.claude/ subdirectories is still open
(https://github.com/anthropics/claude-code/issues/28041).
The bg-isolation Guard
Background sessions in repos without worktree.bgIsolation: "none" block the first Edit/Write against tracked files until the session is inside a worktree. The refusal message names EnterWorktree as the fix.
Two correct responses:
EnterWorktree— default. Branch off, work in isolation, push from the worktree, merge, exit + remove.- Repo-scoped opt-out: write
{ "worktree": { "bgIsolation": "none" } }to the repo's.claude/settings.json. Appropriate when the user has indicated they want in-place edits in this repo (e.g. "keep changes in the local working copy so I can evaluate").
Built-in Isolation Guard (git command shape)
While a session is inside a worktree, Claude Code itself (not a smith hook)
applies four checks to every Bash command
(https://code.claude.com/docs/en/worktrees#how-claude-code-enforces-isolation,
retrieved 2026-09-07): file edits targeting the main checkout; a command
working directory that resolves to the main checkout; git redirected into the
main checkout via git -C, --git-dir, GIT_DIR, GIT_WORK_TREE, or a
cd; and command shape — any command whose text cannot prove that the git
it runs stays inside the worktree. The docs state the command-shape check
cannot be turned off. The refusal reads "This session is isolated in the
worktree … cannot be shown not to be git".
The rtk collision. The rtk hook claude PreToolUse hook rewrites
git … into rtk git …. A launcher with git among its operands is exactly
the shape the guard cannot verify, so every git command in a worktree session
is refused, including rtk proxy git … (upstream report:
https://github.com/rtk-ai/rtk/issues/3864). Fix once, in rtk's config
(~/Library/Application Support/rtk/config.toml on macOS,
~/.config/rtk/config.toml elsewhere):
[hooks]
exclude_commands = ["git"]
Verified 2026-09-07 with rtk hook check: git status, git -C /x status
and git push -u origin main all return No rewrite; gh, ls and the
rest are still rewritten. The bare "git" prefix is not affected by
https://github.com/rtk-ai/rtk/issues/3838 (multi-word prefixes miss the
git -C form). Cost: rtk no longer compresses git output.
What the guard means for the post-merge sync. From inside a worktree,
neither git -C «primary-checkout» pull (guard) nor
git fetch origin «default»:«default» (git refuses to update a branch that
is checked out elsewhere; git fetch --update-head-ok is documented as
internal to git pull) can update the primary checkout's default branch.
ExitWorktree first, then pull in the primary checkout — that is the
Sync-After-Squash-Merge Protocol below. /usr/bin/git … also bypasses the
rtk rewrite but is a workaround, not the fix.
Editing Inside a Worktree (Serena paths)
The bg-isolation guard misses MCP writes. Serena fixes its root at server start (no activate_project in the claude-code context), so after EnterWorktree it still resolves paths against the PRIMARY checkout.
- Prefix Serena paths and globs with the worktree's path:
.claude/worktrees/«name»/src/app.ts. Theworktree-path-guardhook (smith-serena/references/HOOKS.md) blocks one lacking it. - Unreachable worktree (e.g. gitignored): built-in
Edit/Write, ABSOLUTE paths.
worktree.baseRef — fresh vs head
fresh(default) — start fromorigin/«default-branch». Safe when iterating against an up-to-date main. Loses local-only commits that haven't been pushed to origin — and uncommitted changes are stranded either way (see EnterWorktree Semantics; the dirty-guard hook catches this).head— start from localHEAD. Keeps unpushed commits. Use when the user has staged or committed work locally on main that should carry forward into the worktree.
If worktree.baseRef is set in a repo's .claude/settings.json, that repo wins over the user-level setting.
Sync-After-Squash-Merge Protocol
When a PR from a worktree branch is squash-merged to the default branch (main, develop, etc. — never assume main), the local artifacts are inconsistent:
origin/<default-branch>has a new commit with the squashed content.- The local
feat/«name»branch still points at the original (un-squashed) commit; it shows[origin/feat/«name»: gone]aftergit fetch --prune. git branch -d feat/«name»will refuse: "the branch is not fully merged". This is a squash-merge orphan; the content is in main, just under a different SHA.
Protocol:
ExitWorktree({action: "remove", discard_changes: true})— removes the worktree AND its branch (the content is now on the default branch).- From the primary working copy:
git fetch --prune origin && git pull --ff-only. This catches the new commit and removes the stale remote-tracking ref. - Only if a branch survived step 1 (you exited with
keep, renamed it, or created it outside the tool):git branch -D «branch»to clear the squash-merge orphan (git branch -drefuses it as "not fully merged").
If the user pre-mirrored worktree changes back to the main working copy (the "evaluate-in-place" pattern), the working copy has uncommitted edits that are now stale (they're an older version of what's in the merge commit). Clean up:
git checkout -- «tracked-files-from-the-PR»
rm -rf «new-untracked-dirs-from-the-PR»
git pull --ff-only
Operational Worktree Gotchas
- Worktree
.envandnode_modulesare often SYMLINKS to the main repo. A blanketgit add -A/git add .then stages the symlink itself. Stage explicit paths only; never blanket-add inside a worktree. gh pr createrun from a non-primary worktree resets the shell CWD back to the first worktree after it returns. Usegit -C «worktree»for follow-on git ops rather than trusting CWD persistence.
Related
@smith-git/SKILL.md- Git fundamentals; rawgit worktreefor cases outside the Claude Code tools@smith-gh-pr/SKILL.md- PR flow that worktree-based work feeds into@smith-ctx-claude/SKILL.md- Claude Code session model, including background sessions
Before You Finish
Before any EnterWorktree:
git status --porcelain— if non-empty, STOP: commit, stash-and-apply inside the worktree, or branch in place instead. Uncommitted changes never carry into a new worktree (theworktree-dirty-guardhook blocks this).
After EnterWorktree (branch naming):
EnterWorktree({name: "..."})creates branchworktree-{name}which violates Conventional Branch naming (@smith-style).- The
branch-name-guardhook (@smith-git) will warn you to rename immediately:git branch -m <type>/description git branch --show-current # verify before push - Rename before first push —
git pushwill block non-conforming branch names.
On bg-isolation guard refusal:
EnterWorktree({name: "«short-slug»"})— any short name works- Rename branch per the CRITICAL-section rule above:
git branch -m «type»/«description» - Mirror prior uncommitted changes from the main copy via
cponly when the user has asked for "evaluate-in-place"
After squash-merge:
- Confirm the squash commit actually landed first (e.g.
gh pr view «n» --json state,mergedAtshowsMERGED) — don't discard on the assumption a merge command "probably worked." - Then
ExitWorktree(remove + discard — deletes the branch too) →git fetch --prune origin && git pull --ff-onlyon the repo's actual default branch (resolve it, don't assumemain— see Sync-After-Squash-Merge Protocol above). Only if a branch survived (keep/renamed/manual):git branch -D «branch»to clear the squash-merge orphan.