Document how ~/.claude.json derives its per-project key under git worktrees
Documentation Type
Missing documentation (feature not documented)
Documentation Location
https://code.claude.com/docs/en/claude-directory — and the global-config section
of https://code.claude.com/docs/en/settings-reference, which the ~/.claude.json
row on that page links to
Section/Topic
~/.claude.json — how the per-project key is derived under git worktrees
Current Documentation
The .claude directory page describes the file in a single table row:
| ~/.claude.json | Global only | | App state, OAuth, UI toggles, personal MCP servers | Global config |
That is the whole of it, and it does not mention per-project state at all — which
is the part this issue is about. Nothing says how the per-project key is derived,
and nothing says what happens under git worktrees. The page's other mentions of
"worktree" are about .worktreeinclude and orphaned-worktree cleanup, not about
keying.
What's Wrong or Missing?
Two subsystems key per-project state by the same concept, and between them the
behaviour splits three ways rather than two. Only part of it is documented, and
the documented part describes one subsystem's behaviour while reading as though
it covered both.
~/.claude/projects/ does both, in two subdirectories of one tree.
Transcripts split per worktree: each linked worktree gets its own directory
holding its own sessions. The memory/ directory beside them shares: it is
created under the main worktree, shown in the linked-worktree-only run below.
That half is not a new observation — #44130, #30667, #39920 and #48096 each
report it, and all four were closed for inactivity rather than fixed.
The transcript split is the subject of issue #34437, whose commenters quote the
memory documentation as promising the opposite — "The <project> path is derived
from the git repository, so all worktrees and subdirectories within the same repo
share one auto memory directory." On the run below that promise holds for the
memory directory and fails for transcripts, which is why one page can look both
right and wrong depending on which subdirectory a reader has in mind. A commenter
there identified the rule behind the transcript split: the project keys match whatgit rev-parse --show-toplevel returns for each worktree, which is per-worktree
by definition.
~/.claude.json's projects map appears to share: a session started in a
linked worktree resolves to the main worktree's entry. Nothing documents this.
#79794 states the opposite in passing — "this isn't a data problem —~/.claude.json's per-project entries are correctly keyed by full worktree
path". The observations below span
2.1.238, 2.1.241 and 2.1.243, in repositories whose linked worktrees were
created by hand with git worktree add. Two things could reconcile the
two accounts: behaviour that changed after the version #79794 was written
against, or a difference between worktrees Claude Code creates for itself and
worktrees a user adds. A comment on #30667 raises that same distinction.
What was observed, on Claude Code 2.1.238, in a repository with four linked
worktrees:
- Sessions run in two different linked worktrees each wrote their
lastSessionId
onto the main worktree's entry, not onto an entry of their own.
- An approval accepted interactively in one linked worktree was written to the
main worktree's entry and immediately took effect in a third worktree that had
had nothing done to it.
- No linked worktree had an entry of its own, before or after the sessions above,
while every main checkout did. Entries created by hand at linked-worktree paths
had no effect on anything.
Reproduced from scratch on 2.1.241, in a throwaway repository with a single
linked worktree. Sessions were started in both checkouts, and the
external-includes approval was accepted in the linked worktree. Afterwards~/.claude.json carried a projects entry for the main checkout's path and
none for the linked worktree's, and the approval accepted in the linked worktree
appeared on the main checkout's entry ashasClaudeMdExternalIncludesApproved: true.
Corroborated on 2.1.243 before it was withdrawn, with 2.1.241 re-run as a
control, in a fresh throwaway repository with a single linked worktree under an
isolated CLAUDE_CONFIG_DIR. A session was started in each checkout. On both versions
the config file ended up with a projects entry for the main checkout alone
and none for the linked worktree, while the transcript directory beside it
split into one directory per checkout — the two behaviours side by side, from
one pair of sessions.
The sharpest form of it, which rules out the obvious alternative. Run a
session in the linked worktree only, never in the main checkout, against a
config directory that does not yet exist. On both 2.1.243 and 2.1.241 the
resulting projects map holds exactly one key — the main worktree's path —
and none for the worktree the session actually ran in, while that session's
transcript lands under the linked worktree. The memory/ directory beside that
transcript is created under the main worktree, not the linked one, so all
three behaviours are visible in this single run. Because no session ever ran in
the main checkout, the entry cannot be explained as one the main checkout made
for itself. The linked worktree's session wrote it.
That entry is also where the external-includes state lives:hasClaudeMdExternalIncludesApproved andhasClaudeMdExternalIncludesWarningShown are both written onto the main
worktree's key by a session running in the linked one. This is the mechanical
reason hand-setting that flag at a linked worktree's own path does nothing —
nothing ever reads a key there.
On the four-worktree repository this was first found on, the same shape holds
today: projects carries a key for the main checkout and for a parent
directory, and none for any of the four linked worktrees, while a lastSessionId
recorded on the main checkout's key belongs to a session whose transcript is
filed under one of the linked worktrees.
That is inference from behaviour rather than a claim about intent — hence a
documentation issue rather than a bug report.
This is not #29505 again. That issue asked the memory and settings documentation
to cover worktree sharing, and closed. This one is about a file those pages do
not describe at all, as Current Documentation above sets out.
The practical cost is that the file cannot be reasoned about or edited correctly.
Anyone pre-configuring per-project state for a worktree writes it at the
worktree's own path, where nothing reads it, and gets no error and no hint. #9113
reports something of that shape for the trust dialog, drew no maintainer
response, and was auto-closed for inactivity and locked.
Suggested Improvement
State, on the ~/.claude.json description, how the projects key is derived —
and if it resolves through git rev-parse --git-common-dir or equivalent under
worktrees, say so explicitly.
Saying it alongside the ~/.claude/projects/ behaviour would be worth more than
either alone, because the three-way contrast set out above is the confusing part
and a reader currently has no way to know which path does which.
Impact
Medium - Makes feature difficult to understand
Additional Context
Claude Code 2.1.241, Ubuntu 24.04.4 LTS, git 2.43.0. The keying behaviour was
identical on 2.1.243, checked before that build was withdrawn, so it is not
specific to one release.
Found while diagnosing a separate defect that turned out to have this cause.