Worktrees should share the same project directory as the main repo

Status Open
Maintainer reply None cached
Activity 19 comments · opened Mar 14, 2026

Problem

When using git worktrees (either via claude --worktree or manually created), Claude Code creates a separate project directory in ~/.claude/projects/ for each worktree path. This splits conversation history, auto-memory, and project-level settings across multiple isolated directories — even though the worktrees are branches of the same repository.

For example, working in ~/project and then creating a worktree at ~/project-feature-branch produces:

~/.claude/projects/-home-user-project/           # main
~/.claude/projects/-home-user-project-feature-branch/  # worktree — separate conversations, memory, settings

This means:

  • Auto-memory is fragmented — memories saved in a worktree session are invisible to main repo sessions and vice versa
  • Conversation history is split — the /resume picker may show sessions across worktrees, but project-level context is lost
  • Project settings diverge — each worktree gets its own project config

Over time this creates significant knowledge fragmentation, especially for teams using worktrees for parallel development (e.g., running multiple Claude Code sessions in tmux panes on different branches).

Proposed Solution

Detect when a working directory is a git worktree and resolve the project directory to the main worktree's path instead of the current working directory path.

Git already provides this via:

git worktree list --porcelain  # lists all worktrees and identifies the main one
git rev-parse --git-common-dir  # resolves to the shared .git dir

All worktrees of the same repo would then share:

  • ~/.claude/projects/<main-repo-path>/memory/
  • ~/.claude/projects/<main-repo-path>/*.jsonl (conversations)
  • ~/.claude/projects/<main-repo-path>/ (project-level settings)

Alternative: opt-in configuration

If automatic detection is too opinionated, a configuration option would also work:

// ~/.claude/settings.json or project CLAUDE.md
{
  "projectDirectoryStrategy": "git-root"  // vs default "working-directory"
}

Current Workaround

Manually moving .jsonl files, UUID session directories, and memory files from worktree project directories into the main project directory, then deleting the empty source directories. This works but needs to be repeated after every worktree session.

Environment

  • Claude Code CLI
  • Linux (also applicable to macOS)

View original on GitHub ↗

19 Comments

github-actions[bot] · 7 months ago

Found 2 possible duplicate issues:

  1. https://github.com/anthropics/claude-code/issues/15776
  2. https://github.com/anthropics/claude-code/issues/30667

This issue will be automatically closed as a duplicate in 3 days.

  • If your issue is a duplicate, please close it and 👍 the existing issue instead
  • To prevent auto-closure, add a comment or 👎 this comment

🤖 Generated with Claude Code

epechuzal · 7 months ago

+1 — heavy worktree user here and this is a real pain point.

My specific case: I use team-provisioned claude.ai MCP servers (Gmail, Google Calendar, Slack, etc.) which also brings along ~10 MCPs I don't use (Figma, Miro, Notion, Pendo, Postman, etc.). I disable them via disabledMcpServers in the project entry, but every new worktree creates a fresh project entry with an empty disable list — so the unwanted MCPs come back every time.

Two compounding issues:

  1. Settings don't carry over — disabledMcpServers (and other per-project config) needs to be re-applied in every worktree session via /mcp
  2. Dead entries accumulate — nuked worktrees leave orphaned project entries in ~/.claude.json. I just cleaned out 22 dead entries out of 31 total. There's no built-in GC for these.

Resolving the project directory to the main worktree's path (via git rev-parse --git-common-dir) as proposed here would fix both.

jboulter11 · 6 months ago

This, alongside shared permissions, would be great. Just because I make a new worktree, I should not have to also now re-approve all the same stuff and lose my project memories.

nickrahman · 6 months ago

The docs actually say that it should already work this way, so this seems to be a bug.

https://code.claude.com/docs/en/memory#storage-location

Each project gets its own memory directory at ~/.claude/projects/<project>/memory/. The <project> path is derived from the git repository, so all worktrees and subdirectories within the same repo share one auto memory directory. Outside a git repo, the project root is used instead.
yurukusa · 6 months ago

/tmp/issue-34437-comment.md

cderv · 6 months ago

We encountered this while investigating why auto-memory wasn't shared across worktrees on Windows (v2.1.89). The memory docs state:

The <project> path is derived from the git repository, so all worktrees and subdirectories within the same repo share one auto memory directory.

In practice, each worktree gets its own ~/.claude/projects/<key>/ directory — including separate memory, session transcripts, and project settings.

The resulting project keys match what git rev-parse --show-toplevel returns for each worktree, which always gives the worktree's own root rather than the shared repo:

# Main repo
$ git rev-parse --show-toplevel   → /home/user/my-project
$ git rev-parse --git-common-dir  → .git

# Worktree (external sibling dir)
$ git rev-parse --show-toplevel   → /home/user/my-project-feature
$ git rev-parse --git-common-dir  → /home/user/my-project/.git

# Worktree (internal)
$ git rev-parse --show-toplevel   → /home/user/my-project/.worktrees/fix-bug
$ git rev-parse --git-common-dir  → /home/user/my-project/.git

Using --git-common-dir to derive the parent repo path would let all worktrees resolve to the same project key.

Note: on macOS, #39920 reports the opposite for memory — it resolves to the main repo via --git-common-dir, but sessions go to the worktree path. So the behavior appears platform-inconsistent.

Partial workaround for auto-memory only: autoMemoryDirectory in .claude/settings.local.json (per the docs, this setting is not accepted from project-scoped .claude/settings.json):

{
  "autoMemoryDirectory": "~/.claude/projects/<main-project-key>/memory"
}

We automated this with a SessionStart hook that detects worktrees via --git-common-dir and injects the setting if not already present. This only fixes memory sharing — session transcripts and other project data still land in separate directories per worktree.

See also #41283 for a broader discussion of memory identity being tied to filesystem paths.

DesiredPathConsulting · 5 months ago

Another data point — Windows 11, Claude Code v2.1.112, VS Code extension. We hit exactly what @cderv describes in a small internal analytics tool. Worktrees live under .claude/worktrees/<slug> off origin/main (the same path convention claude --worktree uses), and each worktree's CWD ends up with its own ~/.claude/projects/<slug>/ containing isolated memory and .jsonl transcripts.

One Windows-specific detail worth recording here, since the slug rule isn't documented anywhere: on Windows, Claude Code's project-dir slug replaces :, \, /, and . with -, then lowercases the drive letter. The . replacement is the subtle one — a path like ...\.claude\worktrees\feat-foo slugs to ...--claude-worktrees-feat-foo (two dashes at the .claude boundary, not -.claude). We initially shipped a helper that preserved . and had to patch it after observing Claude Code produce a different directory.

Memory — @cderv's autoMemoryDirectory approach is the right shape. We shipped a copy-based SessionStart hook in parallel before seeing that comment; we're migrating to setting-injection. It's live (writes propagate both ways), doesn't require a target directory to pre-exist, and sidesteps most of the slug-rule fragility.

Session transcripts — still a gap. Even with autoMemoryDirectory, a fresh worktree session starts with an empty /resume list. We wrote a helper (scripts/new-worktree.sh, link below) that copies the most-recent .jsonl from the main repo's project dir into the new worktree's project dir at creation time. This covers the specific workflow of "spin up a worktree to pick up where I left off on a different branch." Caveat: tool results in the copied transcript reference paths from the original checkout, which still resolve on disk since the main checkout exists as a sibling, but mid-resume edits target the main checkout rather than the worktree. Documented in our own docs.

Asks, in priority order:

  1. Fix the root cause via the --git-common-dir resolution path @cderv and others have outlined. That closes every shape of this issue (memory, transcripts, permissions, project settings, orphaned worktree dirs) in one change.
  2. Document the slug rule publicly if the root-cause fix is further out. Every userland workaround is reinventing it, and the . edge case isn't obvious.
  3. Expose $CLAUDE_SESSION_ID to hook environments. The current "most-recent .jsonl by mtime" heuristic breaks down with multiple parallel sessions; an explicit session ID at SessionStart would make transcript-copy deterministic.

Full scripts + docs (helper script, SessionStart hook, docs/worktrees.md): https://gist.github.com/DesiredPathConsulting/c9a8ace20a9b619168bc6e3617416601

sepsol · 5 months ago

I wanted to propose a workaround described here. Would like to know your thoughts and/or experience on this.

egolearner · 4 months ago

Workaround: hub directory + --add-dir

Create an empty "hub" directory and always launch Claude Code from there, using --add-dir to point to the actual worktree:

mkdir -p ~/project-hub

# Start a session targeting a specific worktree
cd ~/project-hub && claude --add-dir ~/project/worktree-a --name "my-session"

# Resume later, even switching to a different worktree
cd ~/project-hub && claude --resume "my-session" --add-dir ~/project/worktree-b

All sessions are stored under the hub directory, so they can be shared across worktrees.

Limitations:

  • --add-dir is not persisted — you must re-specify it on every launch/resume.
  • The hub directory has no git context (no branch info, no git status in system prompt), so Claude has to cd into the worktree for git operations.
  • If you accidentally --add-dir multiple worktrees of the same repo simultaneously, Claude may read the wrong files since they share identical relative paths.
MichaelPGifford · 2 months ago

It really sucks that you're working really hard and you've planned every step perfectly. You want to optimize your time by working in parallel, so you spin up 5 or 6 different work trees's to tackle your well planned issues without collision. Then VSCode crashes... All of your claude code sessions get scattered into the 5 or 6 worktrees you created and you have to spend time recovering it. The problem is especially difficult for new users who say, "where has all my work gone"

BasedGPT · 2 months ago

The session files in ~/.claude/projects/ are being keyed from each worktree path, so the same repository ends up with two project slugs. That splits conversation history and project memory even when nothing has been deleted.

I built BasedGPT/claude-code-session-recovery for this path-split family. From the affected Linux machine, run python tools/diagnose.py first and keep the output. Treat it as the starting point with Linux path adjustments, and do not apply a write-bearing command blindly. The exact command it prints will tell you whether the project slugs contain separate transcript files and which repair path is safe.

Do not merge or delete the source directories yet. The repoint_session_to_jsonl.py path is the relevant session-recovery boundary because it repoints metadata to the slug containing each transcript file rather than moving the .jsonl files. The shared-memory and project-settings behaviour still needs an upstream fix.

As a quick check, list the two ~/.claude/projects/ directories and compare their session IDs before changing anything. If both contain different files, preserve both and work from that inventory.

Hope this helps, if my tools are able to help you, would appreciate a ⭐ :)

mklein0 · 1 month ago

Confirming on macOS v2.1.236, and a note that CLAUDE_CODE_PROJECT_DIR_NAME doesn't close this

Same split here on macOS, Claude Code v2.1.236 — newer than the builds cited above (2.1.89 / 2.1.112), so still unfixed. Worktrees live under .claude/worktrees/<slug> (the claude --worktree convention @DesiredPathConsulting describes), and each gets its own ~/.claude/projects/-…-<repo>--claude-worktrees-<slug>/ with separate transcripts. This is the session half of the platform inconsistency @cderv flagged in #39920: on this machine sessions key to the worktree path, so --git-common-dir resolution isn't being applied.

Adding a data point because v2.1.234 introduced CLAUDE_CODE_PROJECT_DIR_NAME, and it doesn't solve this cleanly. What I measured:

  • It's silently ignored unless CLAUDE_CONFIG_DIR is also set — with only CLAUDE_CODE_PROJECT_DIR_NAME, no pinned bucket is created and the transcript still lands in the worktree slug.
  • Setting CLAUDE_CONFIG_DIR=~/.claude to satisfy that makes Claude look for .claude.json inside the config dir (~/.claude/.claude.json) instead of the real ~/.claude.json at home, and spins up a fresh, empty global config (onboarding/migration keys, empty projects). There's no value for CLAUDE_CONFIG_DIR that both enables the pin and preserves the existing global config/MCP state.
  • Even with both vars set, it pins only the transcript bucket — not the project .claude/ settings or the permission allowlist that @jboulter11 and @epechuzal raise.

So the newest native lever is bucket-only and entangled with config relocation. The --git-common-dir root-cause resolution (@cderv, @DesiredPathConsulting's ask #1) is still the fix that closes sessions + permissions + settings in one move.

Workaround I'm using — a variant of @egolearner's hub-dir that keeps full git context. Launch from the main checkout (parent of the shared .git common dir) so the bucket, project .claude/, and permission grants all resolve there, then seed a cd back into the worktree:

wtclaude() {
  local origin common top
  origin="$PWD"
  # Main worktree = parent of the shared .git common dir; every linked worktree
  # points at the same common dir, so this resolves to the same checkout from
  # anywhere. (--git-common-dir may be relative to $PWD — resolve before we cd.)
  common=$(command git rev-parse --git-common-dir 2>/dev/null) \
    || { echo "wtclaude: not in a git repository" >&2; return 1; }
  top=$(command git -C "$common/.." rev-parse --show-toplevel) || return 1
  # Subshell so the parent shell's cwd is untouched after claude exits.
  (
    cd "$top" || return 1
    claude "$@" "cd '$origin' — treat it as the working directory for this session, then wait for my next message."
  )
}

Trade-offs: it burns the first turn on the cd, relies on the model running it, and consumes the positional prompt. Unlike the empty hub dir it keeps git context; unlike autoMemoryDirectory it unifies permissions and project settings too.

On the flag — two shapes would work, and they're duals. The goal is the session bucket at the main repo while the working directory is the worktree, so the two axes must be settable independently:

  • project-dir flag (e.g. --project <dir>, sets only the bucket): launch in the worktree (where claude --worktree already leaves you) and pin the bucket to the main repo — claude --project <main-repo>. No cd needed.
  • working-dir flag (e.g. --cwd <dir>/-C, sets only the working dir, bucket stays at launch cwd): launch in the main repo and point the working dir at the worktree — claude --cwd <worktree>.

Either removes the model-driven cd seed my workaround relies on. Note a git-style -C that changes both (i.e. cd <dir> && claude) does not help — it just re-fragments the bucket to the worktree; the point is the split.

On spelling, for the working-dir route: -C has chdir precedent (git/make/tar/go -C; git even carries both -c and -C), but it's overloaded elsewhere (ssh/grep/ls/curl/rsync/gcc), so --cwd/-chdir (OpenTofu/Terraform) is the safer primary. (cf. #47017's now-closed --cwd request.)

m13v · 1 month ago

collapsing every worktree into the main repo's project dir fixes fragmentation but quietly swaps it for a collision: sessions on different branches now write the same auto-memory and settings at once, so branch-specific context bleeds across branches and last-writer wins. the separate dir is exactly what made worktrees safe to run in parallel. namespacing memory by branch under the shared root gets you the shared history without turning 'fragmented' into 'contaminated'.

jboulter11 · 1 month ago

Yea I want those things shared on purpose. Worktrees don't need individual memory or settings.

mklein0 · 1 month ago

Today the "project" boundary is derived — from cwd, or (where it works) the git toplevel. Another way to think about it is declared: a project is an organizing principle the user holds, and worktrees are the visible case where one principle is spread across several paths.

A use case of how I work: I keep multiple separate clones of the same repo, each dedicated to a purpose — the most common being a clone to do code reviews in. Every session in that clone serves one principle, so I want them pooling the same learnings, permissions, and config. That grouping isn't a worktree relationship — the clones don't share a .git, and the principle could even span different repos.

The --git-common-dir resolution others have outlined is a clean automatic default for the common per-repo case, and I want it too. A declared scope would sit alongside it — the override for when your organizing principle isn't "one repo." The primitive for that already half-exists: CLAUDE_CODE_PROJECT_DIR_NAME (2.1.234) is essentially "declare the scope by name instead of deriving it from path." Per my earlier note it only governs the transcript bucket — not project settings or the permission allowlist — and is entangled with CLAUDE_CONFIG_DIR. Finishing that idea so a declared scope name keys the whole project scope (memory + transcripts + settings + permissions), without requiring a config-dir relocation, would let people point worktrees at one name — and would also cover the multi-clone / by-purpose grouping.

The user/project/enterprise tiers are one convention for organizing config; this is really asking for a user-controlled grouping axis alongside them.

MichaelPGifford · 1 month ago

Here’s what I did.

  1. Worktrees still live in separate directories. Rather than having them live under.claude they live in a normal folder in the branch they are organized under.
  2. There is a master control folder. I open vscode open that folder and all my Claude sessions for all worktrees live in that vscode workspace.

Problems that occur.

  1. Starting a new session does not know which branch or worktree to start working in and can start building in the wrong branch if you don’t clearly specify a branch and worktree name.

Potential solutions.

  1. The new groups feature inside vscode group your sessions by branches and or worktrees
  2. New session hook that prompts you to specify a branch and worktree
m13v · 1 month ago

sharing on purpose is fine until two parallel sessions on different branches flush settings/memory to the same file at once: last-writer-wins silently drops one branch's changes. the sharing you actually want is on reads, the collision only shows up on concurrent writes.

m13v · 1 month ago

the declared-scope idea has a sharp edge on the permissions tier specifically: a read-only review clone and a full-access dev clone under one scope name share one allowlist, so every write/exec grant you approve in dev silently applies in the review clone too. the by-purpose separation you keep those clones apart for is exactly what one shared permission scope erases. memory and transcripts want pooling; permissions are the tier where a per-purpose split is the point.

m13v · 1 month ago

the session-start prompt has a sharp edge: the cwd already pins the worktree deterministically, but a hook that asks you to type the branch reintroduces exactly the mis-pick that scattered your sessions in the first place. with 5-6 parallel trees you'd answer it 5-6 times, and one wrong answer lands work in the wrong tree. resolving from the worktree path beats asking.