[BUG] Windows: per-project state splits into two entries when the cwd's drive-letter case differs (C:\ vs c:\)

Status Fixed / completed
Reported on v2.1.232
Maintainer reply None cached
Activity 2 comments · opened Aug 14, 2026 · closed Aug 14, 2026

Preflight Checklist

  • [x] I have searched existing issues and this hasn't been reported yet
  • [x] This is a single bug report (please file separate reports for different bugs)
  • [x] I am using the latest version of Claude Code

What's Wrong?

## Summary

Claude Code keys per-project state by the **literal cwd string**. On Windows the same directory
can be reported as `C:\Users\me\proj` or `c:\Users\me\proj` depending on how the session was
launched, and each spelling gets its **own** entry in `~/.claude.json` under `projects`, plus its
own directory under `~/.claude/projects/`.

Nothing warns, and the split is silent. The consequences are all of the per-project state:

- `hasTrustDialogAccepted` resets → the trust dialog is shown again for a directory already trusted
- `projectOnboardingSeenCount` resets
- `allowedTools` (every "always allow" the user has clicked for that project) does not apply
- MCP server approvals (`enabledMcpjsonServers` / `disabledMcpjsonServers`) do not carry
- session transcripts land in two different directories under `~/.claude/projects/`

So a user who has already granted trust and approved a set of tools for a project gets asked for
all of it again, with no indication of why.

## Evidence measured on one machine

I deduplicated `~/.claude.json` by hand (18 project keys → 13, merging each pair that differed
only in drive-letter case). **Within 24 hours the duplicates were back**: 18 keys again, with
fresh lowercase twins of four projects.

The twins are provably empty skeletons, not divergent state:

- each lowercase twin: **314 bytes**, only default fields (`hasTrustDialogAccepted: false`,
  `projectOnboardingSeenCount: 0`, empty `allowedTools`)
- the uppercase counterparts carrying the real accumulated state: 786 / 469 / 386 bytes
- `~/.claude/projects/` likewise holds both `C--Users-...` and `c--Users-...` directories for the
  same repository, each with its own transcripts

## What I did NOT isolate

Which launch path produces which spelling. I measured the outcome (two entries, split state),
not the cause. On this machine sessions are started both from a terminal and from the VS Code
extension, and the most recently written entries were the lowercase ones. The bug is independent
of the cause, though: whatever produces a differently-cased drive letter, the state should not
split — Windows paths are case-insensitive.

What Should Happen?

Per-project state should be keyed by a **normalized** path on Windows — at minimum upper-casing
(or lower-casing) the drive letter, ideally case-insensitive comparison of the whole path, which
is what the filesystem itself does.

Two further niceties, in order of value:

1. On startup, if a project key differing only in case already exists, **merge into it** (or
   adopt it) rather than creating a second one. A one-time migration would repair existing splits.
2. Failing that, at least surface it: a session that creates a new project entry whose
   case-insensitive twin already exists could say so, instead of silently presenting a first-run
   experience for a project the user has used for weeks.

**On the merge semantics, since a migration that loses state would be worse than the split.** I
wrote and ran one against my own file, and the field-tested policy is: keep the variant with the
larger serialized payload as the base (it is the one that accumulated real state); for each field
present in both, **union arrays** (`allowedTools`, MCP lists) rather than picking a side; for
booleans take **true** (`hasTrustDialogAccepted` is state gained, never lost); for counters take
the **maximum**. My first version of that script picked "the richer side wins" for scalars too,
and it silently reverted `hasTrustDialogAccepted` from `true` to `false` on two projects — the
dry run caught it, which is the argument for having one. A migration that merges rather than
picks, and prints what it changed, would repair existing splits safely.

Error Messages/Logs

No errors are produced — the split is silent, which is the substance of the report.

What it looks like in ~/.claude.json (abridged, same repository, same machine):

  "C:/Users/me/Projects/my-repo": { ...786 bytes: hasTrustDialogAccepted true,
                                    allowedTools populated, onboarding counted... },
  "c:/Users/me/Projects/my-repo": { ...314 bytes: hasTrustDialogAccepted false,
                                    allowedTools [], projectOnboardingSeenCount 0 }

And in ~/.claude/projects/:

  C--Users-me-Projects-my-repo/    <- transcripts from one set of sessions
  c--Users-me-Projects-my-repo/    <- transcripts from the other

Steps to Reproduce

1. On Windows, start a Claude Code session in a project directory, accept the trust dialog, and
   click "always allow" on some tool call. Exit.
2. Confirm `~/.claude.json` has one entry for that path under `projects`, with
   `hasTrustDialogAccepted: true` and a populated `allowedTools`.
3. Start a session in the **same** directory reached by a path whose drive letter has the other
   case (e.g. `cd c:\Users\me\proj` instead of `C:\Users\me\proj`, or launch through an
   integration that reports the path that way).
4. Observe: the trust dialog appears again, previously-allowed tools prompt again, and
   `~/.claude.json` now has a **second** `projects` entry differing only in the drive letter,
   with default values. `~/.claude/projects/` also has a second transcript directory.
5. The split persists and regenerates: deduplicating the file by hand is undone the next time a
   session starts through the other path.

Claude Model

Opus

Is this a regression?

I don't know

Last Working Version

_No response_

Claude Code Version

claude --version` (2.1.232)

Platform

Anthropic API

Operating System

Windows

Terminal/Shell

PowerShell

Additional Information

- Measured on Claude Code 2.1.229–2.1.232, Windows 11, native install.
- Sessions on this machine are started both from a terminal and from the VS Code extension; the
  most recently created entries were the lowercase-drive ones. I did not isolate which launcher
  reports which spelling — only that the state splits when they differ.
- Practical impact for anyone with a permission policy: per-project `allowedTools` silently not
  applying means users re-approve tool calls they already approved, which trains them to click
  through prompts — the opposite of what the permission layer is for.

View original on GitHub ↗

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