`.claude.json` stores one project directory under up to three different path spellings, splitting trust, MCP servers, and worktree state across them
Summary
On Windows, projects in .claude.json is keyed by a raw path string with no
normalization. Different writers produce different spellings of the same directory, so a
single project ends up stored as two or three independent records that never merge.
The records are not equivalent. The backslash-spelled one carries hasTrustDialogAccepted:; the forward-slash ones carry
truehasTrustDialogAccepted: false and are the only ones that
ever hold mcpServers, hasUnseenTeamArtifacts, and activeWorktreeSession.
The user-visible symptom is that a project appears to "lose" its configured MCP servers and its
active worktree, and re-prompts for trust it was already granted.
Environment
| | |
|---|---|
| OS | Windows 11 Pro 10.0.26200 |
| Claude Desktop | 1.25927.0 (Squirrel, %LOCALAPPDATA%\AnthropicClaude) |
| Config roots inspected | 5 independent ones, via CLAUDE_CONFIG_DIR |
Not related to #86012 (cross-session messaging) or #86556 (Squirrel roll-forward). This is a
data-model defect in .claude.json and reproduces with a single config root.
The three spellings
All three observed for one directory, in one config file:
c:/Users/<user>/Code/<project> lowercase drive, forward slash
C:/Users/<user>/Code/<project> uppercase drive, forward slash
C:\Users\<user>\Code\<project> uppercase drive, backslash
Evidence
Read-only. Run against your own config; it prints only key names and counts.
$f = Join-Path $(if ($env:CLAUDE_CONFIG_DIR) { $env:CLAUDE_CONFIG_DIR } else { "$HOME\.claude" }) '.claude.json'
$o = Get-Content $f -Raw | ConvertFrom-Json -AsHashtable
$byNorm = @{}
foreach ($k in $o.projects.Keys) {
$nk = $k.Replace('\','/').ToLowerInvariant()
if (-not $byNorm.ContainsKey($nk)) { $byNorm[$nk] = @() }
$byNorm[$nk] += $k
}
foreach ($nk in ($byNorm.Keys | Sort-Object)) {
if ($byNorm[$nk].Count -lt 2) { continue }
"=== $nk (stored $($byNorm[$nk].Count) times) ==="
foreach ($k in $byNorm[$nk]) {
$p = $o.projects[$k]
" {0,-46} trustAccepted={1,-6} mcpServers={2,-7} activeWorktreeSession={3}" -f `
$k,
$(if ($p.ContainsKey('hasTrustDialogAccepted')) { $p.hasTrustDialogAccepted } else { 'n/a' }),
$(if ($p.ContainsKey('mcpServers')) { @($p.mcpServers.Keys).Count } else { 'ABSENT' }),
$(if ($p.ContainsKey('activeWorktreeSession')) { 'YES' } else { 'absent' })
}
}
Output, one directory stored three times
=== c:/users/<user>/code/<project> (stored 3 times) ===
c:/Users/<user>/Code/<project> trustAccepted=False mcpServers=0 activeWorktreeSession=YES
C:\Users\<user>\Code\<project> trustAccepted=True mcpServers=ABSENT activeWorktreeSession=absent
C:/Users/<user>/Code/<project> trustAccepted=False mcpServers=0 activeWorktreeSession=YES
hasTrustDialogAccepted is true on exactly the spelling that has no mcpServers key, andfalse on the two that do.
The split is total, not a tendency
Across 5 independent config roots, 46 project keys, 7 colliding directories:
| key spelling | entries | carry mcpServers / hasUnseenTeamArtifacts / activeWorktreeSession |
|---|---|---|
| forward slash | 17 | 17 of 17 |
| backslash | 29 | 0 of 29 |
Every config root that has any backslash entry shows the same trust inversion: backslashtrue, forward-slash false, same directory.
Impact
- Trust state is not reliably keyed to a directory.
hasTrustDialogAcceptedis recorded
per spelling. A directory the user has already trusted reads as untrusted under a different
spelling of the same path. This is a security-relevant control keyed on an unnormalized
string, and it fails in both directions: a redundant prompt is merely annoying, but a trust
decision made for one spelling silently not applying to another is the direction that
matters, and neither is visible to the user.
- Configured MCP servers disappear.
mcpServersexists only on forward-slash entries. A
session resolving the backslash spelling sees none.
- Active worktree is lost.
activeWorktreeSessionexists only on forward-slash entries. A
session resolving the backslash spelling cannot tell which worktree it was working in. This
is the "my project lost its folders" symptom, and it is how the defect actually reaches a
user.
- Silent. Nothing warns that one directory holds multiple records. The file stays valid
JSON and the duplicate keys differ only by case and separator.
Note that ConvertFrom-Json (without -AsHashtable) throws on these files, becausec:/... and C:/... collide under .NET's case-insensitive property handling. Any tooling that
parses .claude.json case-insensitively will fail outright rather than degrade.
Expected behaviour
Normalize the project path before using it as a key — canonical separator and canonical drive
letter case on Windows — and migrate existing entries by merging colliding records. Failing
that, at minimum key trust state on a normalized path, since that one is a security control.
Not reproducible on demand
I cannot give steps that produce a second spelling, because I do not know which code path writes
which form. What I can give is that it is present in 5 of 5 config roots on this machine,
affecting 7 distinct directories, with a completely consistent signature.
This issue has 2 comments on GitHub. Read the full discussion on GitHub ↗