Agent Skills: Claude Code Worktree Tooling

Claude Code worktree TOOLS — EnterWorktree/ExitWorktree, the bgIsolation guard, worktree.baseRef, the branch-naming gotcha, and the squash-merge sync protocol. Use when invoking EnterWorktree/ExitWorktree, hitting the bgIsolation guard, or cleaning up after a worktree-based PR merge. For raw `git worktree` commands see smith-git.

UncategorizedID: tianjianjiang/smith/smith-worktree

Install this agent skill to your local

pnpm dlx add-skill https://github.com/tianjianjiang/smith/tree/HEAD/smith-worktree

Skill Files

Browse the full folder contents for smith-worktree.

Download Skill

Loading file tree…

smith-worktree/SKILL.md

Skill Metadata

Name
smith-worktree
Description
Claude Code worktree TOOLS

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 / ExitWorktree instead of raw Bash git 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-branch from 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 with fatal: '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 with gh pr view «n» --json state rather than trusting the exit code.
  • Every git command 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, no git -C «primary-checkout», no chained cd.
  • For multi-file edits in a background session, the bg-isolation guard will refuse the first Edit and tell the agent to EnterWorktree. 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 -d will refuse it with "not fully merged"; use git branch -D (force) once the squash commit is confirmed on main.
  • Branch naming: EnterWorktree auto-names the branch worktree-«name» which violates @smith-style/SKILL.md. MUST rename before pushing: git branch -m «type»/«description» Alternative: create branch first, then EnterWorktree({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 name param accepts /-separated segments; each segment may contain letters, digits, dots, underscores, dashes only. The branch name is always worktree-«name» regardless.

  • Base ref (worktree.baseRef setting in ~/.claude/settings.json or repo .claude/settings.json):

    • fresh (default since v2.1.133) — branches from origin/«default-branch». Ignores local unpushed commits on main.
    • head — branches from local HEAD. 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 unless discard_changes: true.

  • Operates only on worktrees this session created via EnterWorktree. A worktree created manually with git worktree add is unaffected; use EnterWorktree({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/skills directory 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 .worktreeinclude at the primary checkout root (.gitignore syntax; only paths that match AND are gitignored are copied):
    .claude/skills/*-local/**
    CLAUDE.local.md
    
    The docs apply it to every worktree Claude Code creates with git (--worktree, subagent, desktop parallel sessions); tested 2026-09-24 on v2.1.281 with EnterWorktree and claude --worktree.
  • Gitignore .worktreeinclude itself (global gitignore) — an untracked one makes the checkout dirty, and worktree-dirty-guard then blocks every EnterWorktree.
  • 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 .worktreeinclude cannot share .serena/memories; the link-worktree-memories SessionStart hook does that instead (smith-serena/references/HOOKS.md).
  • A WorktreeCreate hook replaces default creation and skips .worktreeinclude; copy the files in the hook script instead.
  • CLAUDE.local.md alternative 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. The worktree-path-guard hook (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 from origin/«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 local HEAD. 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] after git 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:

  1. ExitWorktree({action: "remove", discard_changes: true}) — removes the worktree AND its branch (the content is now on the default branch).
  2. 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.
  3. 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 -d refuses 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 .env and node_modules are often SYMLINKS to the main repo. A blanket git add -A / git add . then stages the symlink itself. Stage explicit paths only; never blanket-add inside a worktree.
  • gh pr create run from a non-primary worktree resets the shell CWD back to the first worktree after it returns. Use git -C «worktree» for follow-on git ops rather than trusting CWD persistence.

Related

  • @smith-git/SKILL.md - Git fundamentals; raw git worktree for 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 (the worktree-dirty-guard hook blocks this).

After EnterWorktree (branch naming):

  • EnterWorktree({name: "..."}) creates branch worktree-{name} which violates Conventional Branch naming (@smith-style).
  • The branch-name-guard hook (@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 push will block non-conforming branch names.

On bg-isolation guard refusal:

  1. EnterWorktree({name: "«short-slug»"}) — any short name works
  2. Rename branch per the CRITICAL-section rule above: git branch -m «type»/«description»
  3. Mirror prior uncommitted changes from the main copy via cp only 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,mergedAt shows MERGED) — 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-only on the repo's actual default branch (resolve it, don't assume main — 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.