Worktrees should share the same project directory as the main repo
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
/resumepicker 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)
19 Comments
Found 2 possible duplicate issues:
This issue will be automatically closed as a duplicate in 3 days.
🤖 Generated with Claude Code
+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
disabledMcpServersin 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:
disabledMcpServers(and other per-project config) needs to be re-applied in every worktree session via/mcp~/.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.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.
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
/tmp/issue-34437-comment.md
We encountered this while investigating why auto-memory wasn't shared across worktrees on Windows (v2.1.89). The memory docs state:
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-toplevelreturns for each worktree, which always gives the worktree's own root rather than the shared repo:Using
--git-common-dirto 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:
autoMemoryDirectoryin.claude/settings.local.json(per the docs, this setting is not accepted from project-scoped.claude/settings.json):We automated this with a
SessionStarthook that detects worktrees via--git-common-dirand 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.
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>offorigin/main(the same path conventionclaude --worktreeuses), and each worktree's CWD ends up with its own~/.claude/projects/<slug>/containing isolated memory and.jsonltranscripts.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-fooslugs to...--claude-worktrees-feat-foo(two dashes at the.claudeboundary, 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
autoMemoryDirectoryapproach 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/resumelist. We wrote a helper (scripts/new-worktree.sh, link below) that copies the most-recent.jsonlfrom 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:
--git-common-dirresolution path @cderv and others have outlined. That closes every shape of this issue (memory, transcripts, permissions, project settings, orphaned worktree dirs) in one change..edge case isn't obvious.$CLAUDE_SESSION_IDto hook environments. The current "most-recent.jsonlby 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/c9a8ace20a9b619168bc6e3617416601I wanted to propose a workaround described here. Would like to know your thoughts and/or experience on this.
Workaround: hub directory +
--add-dirCreate an empty "hub" directory and always launch Claude Code from there, using
--add-dirto point to the actual worktree:All sessions are stored under the hub directory, so they can be shared across worktrees.
Limitations:
--add-diris not persisted — you must re-specify it on every launch/resume.cdinto the worktree for git operations.--add-dirmultiple worktrees of the same repo simultaneously, Claude may read the wrong files since they share identical relative paths.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"
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-recoveryfor this path-split family. From the affected Linux machine, runpython tools/diagnose.pyfirst 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.pypath is the relevant session-recovery boundary because it repoints metadata to the slug containing each transcript file rather than moving the.jsonlfiles. 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 ⭐ :)
Confirming on macOS v2.1.236, and a note that
CLAUDE_CODE_PROJECT_DIR_NAMEdoesn't close thisSame 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>(theclaude --worktreeconvention @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-dirresolution 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:CLAUDE_CONFIG_DIRis also set — with onlyCLAUDE_CODE_PROJECT_DIR_NAME, no pinned bucket is created and the transcript still lands in the worktree slug.CLAUDE_CONFIG_DIR=~/.claudeto satisfy that makes Claude look for.claude.jsoninside the config dir (~/.claude/.claude.json) instead of the real~/.claude.jsonat home, and spins up a fresh, empty global config (onboarding/migration keys, emptyprojects). There's no value forCLAUDE_CONFIG_DIRthat both enables the pin and preserves the existing global config/MCP state..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-dirroot-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
.gitcommon dir) so the bucket, project.claude/, and permission grants all resolve there, then seed acdback into the worktree: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; unlikeautoMemoryDirectoryit 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>, sets only the bucket): launch in the worktree (whereclaude --worktreealready leaves you) and pin the bucket to the main repo —claude --project <main-repo>. Nocdneeded.--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
cdseed my workaround relies on. Note a git-style-Cthat 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:
-Chas chdir precedent (git/make/tar/go -C;giteven carries both-cand-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--cwdrequest.)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'.
Yea I want those things shared on purpose. Worktrees don't need individual memory or settings.
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.
Here’s what I did.
Problems that occur.
Potential solutions.
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.
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.
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.