Skip to content

Worktrees

Give each Claude Code session its own git worktree so parallel work never collides, plus subagent isolation, .worktreeinclude, cleanup and recovery.

A git worktree is a second working directory attached to the same repository. It has its own files and its own branch, but shares history and remotes with your main checkout. Put each Claude Code session in its own worktree and one session can build a feature while another fixes a bug, without either touching the other's files.

Worktrees isolate file edits. They pair well with subagents (splitting work inside one session) and cross-session messaging (sharing findings between sessions). Running agents in parallel compares all the options.

Note: Worktrees need a git repository. For SVN, Perforce or Mercurial, replace the git logic with hooks (see Other version control systems). In the desktop app, pick the worktree option when starting a session.

Most days you only need the first two sections: start a session in a worktree, and clean up when you leave.

Starting a session in a worktree

claude --worktree invoice-export

--worktree (or -w) creates .claude/worktrees/invoice-export/ at the repository root, on a new branch called worktree-invoice-export, and starts Claude there. Open another terminal and run it again with a different name for a second isolated session. Leave out the name and Claude invents one, something like bright-running-fox.

Interactive runs need workspace trust. If you have never run Claude in the repository, run plain claude once and accept the trust dialog, otherwise --worktree exits with an error asking you to. claude -p --worktree skips the trust check.

Tip: Add .claude/worktrees/ to .gitignore so worktree contents do not show up as untracked files in your main checkout.

Preparing the environment

A worktree starts as a clean checkout, so dependencies are not installed and ignored files are missing. Ask Claude to run your setup (npm ci, uv sync or whatever your project uses), or run it yourself inside the worktree folder. To copy files like .env across automatically, use .worktreeinclude.

Asking Claude to switch mid-session

Say "work in a worktree" and Claude creates one with the EnterWorktree tool. From inside a worktree it can jump to another one under .claude/worktrees/ the same way; the one it left stays on disk.

If Claude tries to enter a path outside .claude/worktrees/, you are asked first, because that move takes the working directory, write access and project config (CLAUDE.md, settings) with it. Neither an EnterWorktree permission rule nor "don't ask again" suppresses this prompt; only bypassPermissions mode does. Before v2.1.206 there was no prompt.

Note: Hook paths do not move with the session. ${CLAUDE_PROJECT_DIR} keeps pointing at the project root where the session began, so ${CLAUDE_PROJECT_DIR}/.claude/hooks/lint.sh still runs the main checkout's copy. The cwd field in the hook's input JSON does follow Claude into the worktree (and on each cd), so read that when a hook needs the worktree path. See hooks.

Cleaning up on exit

When you leave an interactive worktree session, Claude Code checks for anything that removal would destroy: modified or untracked files, uncommitted work in checked-out submodules, and new commits.

Worktree stateWhat happens
Clean, unnamed sessionWorktree and branch are removed automatically
Clean, named sessionYou are asked whether to keep it
Has work in itYou choose keep or remove. Keep preserves the folder and branch, and Claude Code prints a claude --worktree <name> --resume command to come back. Remove deletes both, work included
State cannot be checkedYou are asked, and told what could not be inspected

These rules cover worktrees Claude made with git. Hook-created worktrees are handled by your WorktreeRemove hook instead.

Runs with -p have no exit prompt, so their worktrees are left behind with the lock Claude Code took at creation. A later session's stale-lock sweep releases that lock. To remove one yourself, run git worktree remove, and git worktree unlock first if git complains it is locked.

On Windows, removal only deletes things inside the worktree. A directory symlink or NTFS junction inside it is removed as a link, and its target is left alone. Before v2.1.205 a nested link could take its target with it.

Resuming a worktree session

If a session ended inside a worktree without exiting it, resuming puts you back in that worktree. That applies to interactive resumes, to --continue and --resume with -p (from v2.1.212), and to the Agent SDK. --continue picks the latest session recorded under the directory you launch from. Once back, Claude can leave with the ExitWorktree tool.

Claude Code first checks that the worktree is still a separate checkout from the main one, and will not re-enter one that fails. A few things change the outcome:

  • Where you launch from. Resume from the main checkout or anywhere in the repository. Worktrees Claude created under .claude/worktrees/ can be re-entered even from inside. For other worktrees (its own repository, no git metadata, or a subfolder of one made by git worktree add), launch from the main checkout.
  • --fork-session. The fork starts in your launch directory and leaves the original worktree alone.
  • Worktree deleted. You resume in the launch directory, are told the worktree is gone, and the binding is cleared.

From v2.1.198, when Claude enters or exits a git worktree Claude Code created, the transcript moves to the new working directory (the same way /cd works), so --resume and /desktop find it. Hook-created worktrees keep their transcript at the launch directory.

How isolation is enforced

While a session is in a worktree, Claude Code refuses certain tool calls. This applies however you got there (--worktree, EnterWorktree or a resume), in interactive and background sessions, and to every subagent spawned from the session.

CheckWhat it blocks
File editsEdit, Write or NotebookEdit aimed at a path in the main checkout
Working directoryBash, PowerShell or Monitor commands that run in the main checkout, or whose directory cannot be proven to stay outside it
Git redirectsBash or Monitor commands that point git at the main checkout via git -C, --git-dir, GIT_DIR, GIT_WORK_TREE or a cd first
Command shapeBash or Monitor commands where Claude Code cannot tell from the text that git stays in the worktree, such as a computed command name, unparseable syntax, or expansions like ${!name} or ${ command; }. This one cannot be disabled

PowerShell commands only get the working directory check. The checks cover the repository you launched from and the main checkout a linked worktree belongs to.

None of these follow what a command writes. A cp or > redirect into the main checkout, with no git involved, is treated as an ordinary shell command and governed by your permission mode and rules. Refusals reach Claude as a tool error naming the worktree and explaining how to proceed; see errors for the message.

Giving subagents their own worktrees

Parallel subagents editing the same tree will trip over each other. Ask Claude to "use worktrees for your agents", or make it permanent for a custom agent with isolation: worktree in its frontmatter:

---
name: api-migrator
description: Moves endpoint handlers from the v1 router to the v2 router
isolation: worktree
---

Move the handlers you are given to the v2 router, update their tests,
run the test suite and report which handlers passed.

Each subagent gets a temporary worktree. If it finishes with no changes the worktree is removed straight away; if it made changes, the worktree stays until the periodic sweep can remove it safely. Subagent worktrees use the same base as --worktree, so they branch from the default branch unless worktree.baseRef is "head".

A subagent in its own worktree takes its startup instruction files from your main conversation, not the worktree. When the worktree is in the default .claude/worktrees/ location, it also does not pick up the worktree's root CLAUDE.md or .claude/rules/ as it reads files, even if they differ on that branch.

The cleanup sweep

A periodic sweep removes worktrees Claude made for subagents and background sessions once they are older than cleanupPeriodDays. Backgrounding a --worktree session turns its worktree into a background-session worktree that the sweep may collect.

The sweep leaves a worktree alone when:

  • it still has modified or untracked files, or unpushed commits;
  • a checked-out submodule has changes, or submodules cannot be inspected (v2.1.274 or later);
  • Claude Code cannot work out the repository's filter drivers or finds a config setting it cannot neutralise (the same cases that block creation, see Git LFS);
  • it belongs to a --worktree session you have not backgrounded;
  • you created it yourself with git worktree add, even if you later ran and backgrounded a session in it.

Claude Code marks every worktree it creates in its git metadata and never sweeps an unmarked one, which includes hook-created worktrees.

While an agent or backgrounded session runs, Claude Code holds a git worktree lock on its worktree, so neither the sweep nor git worktree remove can take it. The sweep releases locks left by dead processes (from v2.1.210), but never a lock you set yourself. To clear a worktree the sweep keeps, use git worktree remove, with --force for uncommitted or untracked files, after git worktree unlock if needed.

Changing how worktrees are created

By default worktrees go in .claude/worktrees/, branch from the default branch and contain only tracked files. These options change that.

Base branch

worktree.baseRef in settings takes one of two values:

ValueBranches from
"fresh" (default)The remote default branch, usually main, for a clean tree matching the remote
"head"Your current local HEAD, including unpushed commits. Inside a worktree it means that worktree's HEAD

You cannot give it a branch name; use git directly for that. To make worktrees carry your in-progress work, which is handy for subagents:

{
  "worktree": { "baseRef": "head" }
}

With "fresh", Claude Code refreshes origin/HEAD if the repository has not been fetched in 24 hours, giving the fetch five seconds and falling back to the cached ref. The fetch never waits for terminal input, so a password, passphrase or new SSH host prompt counts as failure. With no remote, or no cached origin/HEAD and a failed fetch, it falls back to local HEAD. Before v2.1.208 it never fetched.

Starting from a pull request

Pass --worktree a PR or MR number with #, a GitHub PR URL, or a GitLab MR URL. Quote it so the shell does not read # as a comment:

claude --worktree "#871"

The worktree lands at .claude/worktrees/pr-871. Only the number is used, and the fetch always comes from origin:

origin hostRef fetched
github.compull/<number>/head
gitlab.commerge-requests/<number>/head
Anything else (GitHub Enterprise, self-managed GitLab)pull/<number>/head, then merge-requests/<number>/head

This fetch also refuses to prompt. If credentials or a host check are needed it fails with Error creating worktree: Failed to fetch PR/MR #<number>. Keys held in ssh-agent work fine; run git fetch once by hand to accept a new host. GitLab URLs and the fallback order arrived in v2.1.233.

Copying ignored files into new worktrees

Create .worktreeinclude at the project root using .gitignore syntax. A file is copied only if it matches a pattern and is also gitignored, so tracked files are never duplicated.

.env.development
.npmrc
certs/localhost.pem

A note on **/ patterns: when the target files live inside a directory that is ignored as a whole, they are copied only if that directory matches the pattern, or the first name after **/ appears in the directory's path. **/.claude/skills/*.md reaches into an ignored .claude/ because the first name is .claude. If a **/ pattern does not reach, name the directory: tools/**/settings.json rather than **/settings.json. That second rule arrived in v2.1.239.

.worktreeinclude applies to every worktree Claude Code makes with git: --worktree, subagent worktrees and desktop parallel sessions. It is ignored when a WorktreeCreate hook is in charge, so copy files in the hook.

Reusing a name

If the named worktree folder already exists, --worktree opens it rather than making a new one. With the "fresh" base it resets to the default branch, instead of the old tip, only when all of these hold:

  • no uncommitted or untracked files;
  • still on the branch Claude Code created;
  • no commits of its own, or its PR/MR was merged and the remote branch deleted (judged from git alone: the pushed branch is gone and every commit is on the default branch).

Otherwise, or when state cannot be verified, the base is "head", or the name is a PR reference, it reopens at the old tip. Before v2.1.208 it always reopened at the old tip.

Replacing creation with a hook

A WorktreeCreate hook replaces the git logic entirely and can put worktrees anywhere. See the next section.

What a worktree shares with the main checkout

  • The .git directory. Git commands in a worktree write to the shared .git, and the sandbox allows those writes, so git commit works with sandboxing on.
  • Project-scope plugins installed from the main checkout (v2.1.200 or later).
  • Permission approvals. "Yes, and don't ask again" for a Bash command saves to the main checkout's .claude/settings.local.json, so it applies everywhere in the repo and survives removal (from v2.1.211). On Windows, and other cases where Claude Code does not use the repository root, it stays with the worktree.
  • Untracked skills, agents and commands. If the worktree has no .claude/skills at its root (because yours is gitignored, say), the main checkout's project skills load instead; a worktree with its own copy uses only that. The same goes for .claude/agents and .claude/commands. The skills part needs v2.1.277 or later.

All of this holds for --worktree, git worktree add and desktop worktrees alike.

Creating worktrees by hand

Use git directly when you want a particular existing branch or a location outside the repository:

# new branch alongside the repo
git worktree add ../shop-checkout-redesign -b checkout-redesign

# existing branch
git worktree add ../shop-hotfix hotfix/vat-rounding

cd ../shop-checkout-redesign && claude

git worktree list
git worktree remove ../shop-checkout-redesign

The full command reference is in the git worktree documentation.

Other version control systems

For SVN, Perforce, Mercurial and the like, configure WorktreeCreate and WorktreeRemove hooks. The create hook receives JSON on stdin including the name, does whatever checkout it needs, and prints the directory path on stdout. Anything else should go to stderr. Here is a Mercurial version:

{
  "hooks": {
    "WorktreeCreate": [
      {
        "hooks": [
          {
            "type": "command",
            "command": "bash -c 'N=$(jq -r .name); D=\"$HOME/hg-work/$N\"; hg clone -q ~/code/billing \"$D\" >&2 && echo \"$D\"'"
          }
        ]
      }
    ]
  }
}

Pair it with a WorktreeRemove hook that deletes the clone. .worktreeinclude is not processed in this mode.

A WorktreeCreate hook also lets /batch run outside git (v2.1.281 or later): each /batch subagent publishes its change with your VCS commands and reports what it published when it cannot open a pull request.

Troubleshooting

Cannot enter the worktree at startup

Claude Code prints the path and exits with code 1. Usually a WorktreeCreate hook printed something besides the directory, or the directory was deleted after creation.

Creation fails on a symlinked path

Worktree creation is refused if .claude, .claude/worktrees or the worktree folder is a symlink, and the error names it. Remove the link and retry. Before v2.1.212 a committed symlink could send files outside the repository.

Git LFS files are only pointers

If you ran git lfs install --local, the LFS filter lives in the repository's own .git/config, and Claude Code deliberately skips repository-level filter drivers when creating a worktree, because a filter driver is a shell command that anything with write access (Claude included) could have planted. A global git lfs install is unaffected. Run git lfs pull inside the worktree to fetch the real files. Before v2.1.247 those drivers did run.

In four rare cases no worktree is created at all:

ErrorFix
Could not read the repository git config to neutralize filter driversFix permissions on .git/config
The repository git config defines a filter driver whose name cannot be neutralized (contains "=" or a newline)Rename or remove that driver
The repository git config has a conditional include (includeIf)Inline what the includeIf pulls in and remove it (global includeIf is fine)
Git was not run: the repository's own git config sets <key>A key such as lfs.customtransfer.<name>.path or lfs.standalonetransferagent points at a program. Move it to global config if it is yours; delete it if you do not recognise it

"Refusing to use as an isolation worktree"

Claude Code checked the directory's git identity and decided git commands there could hit the main checkout, for example a .git file pointing at the main repository's .git, or a core.worktree redirect. An unreadable .git entry is also refused. A hook-created folder with no git metadata passes only if it is not inside any repository, so have your hook create folders outside repositories. The refused directory is always left in place.

Message containsWhat to do
launch from the parent checkout or Run the resume from the project checkoutLaunch from the main checkout; nothing needs recreating
it cannot be resumed or re-enteredRecreate the worktree, or resume from its parent checkout if it has one
it contains the protected checkoutThe path is a parent of your checkout (your home folder, say). Do not delete it; change the hook output or EnterWorktree target
the protected checkout <path> has a .git entry that could not be examined or has git metadata that could not be resolvedThe main checkout is the problem (permissions, dubious ownership). Fix it and ignore the advice to recreate
its recorded path has a network spellingRecreate at a local path
Anything elseFollow the named fix. Rule out symlinks in the path or git failing to run before deleting anything, and salvage changes first

Resume lands outside the worktree

Interactively you see one of these:

Message starts withMeaning
Your worktree <path> no longer existsIt was deleted; binding cleared, nothing to do
Could not verify your worktree <path> this timeProbably transient; binding kept, resume again
Did not re-enter your worktree <path>Judged unsafe; binding cleared. Match the refusal above
Could not re-enter your worktree <path>Cannot vouch from where you launched; binding kept. Match the refusal above

Clearing a binding is recorded in the transcript. If transcript writes are suppressed the message says the clear could not be saved and the worktree will be checked again next time.

With -p and SDK resumes, every refusal except a missing worktree stops the resume with a stderr error rather than carrying on unisolated:

  • Error: cannot resume into worktree <path>: ...This session was not started. (the Did not re-enter case). The binding is cleared so the next resume proceeds without isolation. If transcripts are suppressed, it suggests --fork-session or a new conversation instead.
  • Error: could not verify worktree <path> for this resume, so the resume was aborted...
  • Error: ...The worktree binding is kept. (the Could not re-enter case)
  • Notice: the worktree <path> for this session no longer exists..., after which the session continues.

With --output-format stream-json the refusal also arrives as a result message with subtype error_during_execution and the text in errors (v2.1.260 or later). Its startup_failure_reason is worktree_unverified for the verify failure and worktree_resume_refused for the other two (v2.1.274 or later), so SDK code can branch on that instead of parsing text.