anthropics / anthropics/claude-code

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

Open
#88,418 5 comments 0 reactions 0 assignees View on GitHub
area:core area:mcp area:security bug has repro platform:windows
Dominant language
Python
Stars
145k
Forks
23.1k
PR merge metrics
PR metrics pending

Description

## 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//Code/ lowercase drive, forward slash
C:/Users//Code/ uppercase drive, forward slash
C:\Users\\Code\ uppercase drive, backslash
```

## Evidence

Read-only. Run against your own config; it prints only key names and counts.

```powershell
$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//code/ (stored 3 times) ===
c:/Users//Code/ trustAccepted=False mcpServers=0 activeWorktreeSession=YES
C:\Users\\Code\ trustAccepted=True mcpServers=ABSENT activeWorktreeSession=absent
C:/Users//Code/ 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.
2. **Configured MCP servers disappear.** `mcpServers` exists only on forward-slash entries. A
session resolving the backslash spelling sees none.
3. **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.
4. **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.

Contributor guide

No contributing guide indexed for this repository

Research direction

Run the provided PowerShell inspection against your own .claude.json and confirm how duplicate project keys and their fields differ. Trace the code paths that write project keys, then verify that Windows spellings normalize to one record and that existing colliding entries retain trust, MCP server, and worktree state after migration.

Written by the indexing model from the issue text.

Assessment

Tech stack
python
Domain
cli, operating-systems
Issue type
Bug
Difficulty
4/5
Estimated time
3-5 days
Activity status
Active
Clarity
Mostly clear
Newbie friendliness
35/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.