`.claude.json` stores one project directory under up to three different path spellings, splitting trust, MCP servers, and worktree state across them

Status Open
Maintainer reply None cached
Activity 2 comments · opened Aug 21, 2026

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:
true
; the forward-slash ones carry hasTrustDialogAccepted: 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, and
false 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: backslash
true, forward-slash false, same directory.

Impact

  1. Trust state is not reliably keyed to a directory. hasTrustDialogAccepted is 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.

  1. Configured MCP servers disappear. mcpServers exists only on forward-slash entries. A

session resolving the backslash spelling sees none.

  1. Active worktree is lost. activeWorktreeSession exists 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.

  1. 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, because
c:/... 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.

View original on GitHub ↗

This issue has 2 comments on GitHub. Read the full discussion on GitHub ↗