[BUG] Windows drive-letter case not canonicalized in project keys — duplicate .claude.json / installed_plugins.json entries; VS Code trust silently dropped
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?
On Windows, project paths are used as object keys in ~/.claude.json (projects) and in ~/.claude/plugins/installed_plugins.json (projectPath / installPath), but the drive letter is not case-canonicalized before the key is stored or compared. The stored case depends entirely on which surface launched the process:
- VS Code extension spawns the CLI with
cwd: workspaceFolder.uri.fsPath, and VS Code'sfsPathlowercases the drive letter →c:\Users\... - git-bash / MINGW / Cygwin paths (
/c/...,/cygdrive/c/...) are run through a POSIX→Windows translator that uppercases the drive →C:\Users\... - PowerShell / cmd already report uppercase
C:\...
Because lookups are case-sensitive (projects?.[key] and projectPath === o), the same physical directory ends up stored under two different keys (c:/... and C:/...). Windows paths are case-insensitive, so these are the same directory — but Claude treats them as distinct.
Two concrete user-visible symptoms:
- Workspace trust silently dropped in VS Code. I accepted the trust dialog (my
~/.claude.jsoncontains"C:/Users/.../api-specs": { hasTrustDialogAccepted: true }and"C:\\Users\\...\\api-specs": { ... }). But the VS Code extension computes the keyc:/Users/.../api-specs(lowercase, fromfsPath), which doesn't match, so trust reads asfalseand 32permissions.allowentries from.claude/settings.jsonwere dropped. The error even says "workspace not yet trusted" when in fact it was trusted twice — under a different-cased key.
- Duplicate / mixed-case entries in
installed_plugins.json.projectPathandinstallPathare stored raw (native backslashes, original case, never normalized), so the same file showsC:\\Users\\...on one line andc:\\Users\\...on the next. Case-sensitive===lookups then miss, causing duplicate entries / re-installs.
What Should Happen?
The drive letter (and ideally the whole path) should be canonicalized to a single case before being used as a stored key or in any equality comparison, so that one physical directory maps to exactly one entry regardless of launch surface (VS Code extension, git-bash, PowerShell, cmd).
Concretely: a workspace trusted once should stay trusted when opened from the VS Code extension, and a plugin installed once should not be duplicated/re-installed when the path case differs.
Error Messages/Logs
2026-07-08T19:36:36.105Z [DEBUG] Dropped 32 project-scoped permissions.allow entries — workspace not yet trusted
Ignoring 32 permissions.allow entries from .claude/settings.json: this workspace has not been trusted. Run Claude Code interactively here once and accept the trust dialog, or set projects["c:/Users/<user>/repos/api-specs"].hasTrustDialogAccepted: true in ~/.claude.json
Note the error references lowercase `c:/Users/...` (from VS Code `fsPath`), while `~/.claude.json` already contains a trusted entry keyed with uppercase `C:/Users/...`.
Steps to Reproduce
Root cause (from decompiled claude.exe) — the path-key normalizer folds separators but not drive case:
function doe(e){
let t = normalize(e);
if (platform === "windows") return t.replaceAll("\\", "/"); // separators only — drive case untouched
return t;
}
Trust reader and writer both funnel through doe(), so they agree with each other — but doe()'s output is not canonical, so a different-cased cwd yields a different key:
// writer
function WIt(e){ let t = doe(path.resolve(e)); /* set projects[t].hasTrustDialogAccepted = true */ }
// reader
function Rse(){ let e = doe(...cwd...); return config.projects?.[e]?.hasTrustDialogAccepted === true; }
The case divergence comes from a POSIX→Windows translator that uppercases the drive, which only fires for POSIX-shaped input (git-bash/Cygwin) and not for native fsPath:
// /cygdrive/c/... and /c/... → C:\... (uppercased)
// c:\... (VS Code fsPath) passes through unchanged → stays lowercase
Reproduction:
- On Windows, open a project in a terminal where the drive is uppercase (PowerShell, cmd, or git-bash — git-bash uppercases via the translator). Run
claude, accept the trust dialog. →~/.claude.jsonnow hasprojects["C:/Users/you/proj"].hasTrustDialogAccepted = true. - Open the same folder with the Claude Code VS Code extension (which spawns the CLI with
cwd = uri.fsPath, lowercasing the drive toc:\...). - Observe: the extension reports the workspace as untrusted and drops the project-scoped
permissions.allowentries from.claude/settings.json, even though it was trusted in step 1. The error citesprojects["c:/Users/you/proj"](lowercase) — a different key than what was stored.
Plugins variant:
- Install a plugin scoped to a project from a lowercase-drive context (VS Code) →
installed_plugins.jsonstoresprojectPath: "c:\\Users\\...". - Inspect
installed_plugins.jsonafter any operation from an uppercase-drive context → you'll see mixedC:\\.../c:\\...entries (installPathupper,projectPathlower in the same file), and case-sensitive lookups (l.projectPath === o) miss, producing duplicates.
Suggested fix: canonicalize the drive letter (and folded case) inside doe(), e.g. t.replace(/^([a-z]):/, (_,d) => d.toUpperCase() + ":"), and route plugin projectPath/installPath storage + comparison through the same canonicalizer instead of storing/comparing raw strings. Since trust read+write both go through doe(), that single choke point fixes the trust duplication; the plugins file needs the same normalization applied at its store/compare sites (it currently bypasses doe() entirely). related #74912 #74612
Claude Model
Not sure / Multiple models
Is this a regression?
I don't know
Last Working Version
n/a
Claude Code Version
2.1.204
Platform
AWS Bedrock
Operating System
Windows
Terminal/Shell
VS Code integrated terminal
Additional Information
(Also reproducible cross-surface: the mismatch specifically arises between the VS Code extension's fsPath cwd and git-bash/PowerShell launches.)
---
Additional Information
- The bug spans multiple config files, so a fix should target the shared path-key normalization rather than any single call site:
~/.claude.json→projectsobject keys (trust,permissions.allow, onboarding state)~/.claude/plugins/installed_plugins.json→projectPath/installPath(stored raw, backslashes + original case)- The whole binary has no drive-letter case-folding anywhere on these paths (grep for
normalizeDriveLetter/ drive case-fold → none);doe()only swaps\→/. - The user-facing error message is misleading: it says "workspace not yet trusted" when the workspace was trusted, just under a differently-cased key. If detectable, it would help to say "trusted under a different-cased path" and point at the case mismatch.
- The VS Code extension itself contains no trust-handling logic (no
hasTrustDialogAccepted/workspaceTrustreferences inextension.js); it fully delegates to the CLI's cwd-derived key — so the only reliable user workaround today is hand-adding the lowercase-drive key to~/.claude.json, which is not discoverable from the extension UI.
related #67749 but my issue is based on Workspace Trust and Plugin loading, not MCPs, specifically.
related #69066
area:cli
area:vscode
area:plugins
Showing cached comments. Read the full discussion on GitHub ↗
5 Comments
@dflor003 any chance you can verify if this regression was also from 2.1.181 with the tests you ran? Thanks.
Yes, I can confirm that it broke between 2.1.179 and 2.1.181. I was able to fix it by downgrading to 2.1.179.
Independent confirmation on Windows 11 + VS Code extension 2.1.214, with one
finding that I think refines the model in this issue.
The casing is not fixed per launch surface — it oscillates within a single
session.
This issue (and #76994) describe the stored case as determined by whichever
surface launched the process: CLI/git-bash → uppercase, VS Code
fsPath→lowercase. That's correct as far as it goes, but it implies one process yields
one casing. It doesn't. A single VS Code session records both, alternating
throughout.
One ~6-hour session,
cwdvalues across its transcript:Fully interleaved — 78 transitions between casings, each correlating with a
shell tool call:
Controlled test. Measured live, using non-shell tools to take the
measurements so the act of measuring didn't perturb the result:
"cwd":"w:= 102,"cwd":"W:= 61(Get-Location).Path→ returned
W:\"cwd":"W:= 63So it isn't specific to the git-bash POSIX translator named in this issue —
PowerShell does it too. The extension supplies a lowercase baseline, and any
shell tool invocation reports the true on-disk casing back.
Why this matters for the fix: canonicalizing at process start is not
sufficient. A user who only ever opens VS Code and never touches the CLI still
generates both keys, because the divergence happens mid-session, between the
session baseline and the shell layer. The
cwdneeds to be resolved once andused everywhere, in addition to canonicalizing before keying.
Control group — sessions that never acquired a lowercase baseline are
uniformly single-cased, which is consistent with the mechanism:
Not drive-type dependent. Several related issues (#62288, #63527, #76205)
frame this around mapped network drives. On this machine the oscillation happens
across all three drive types, so mapping is not a factor:
| drive |
Win32_LogicalDiskDriveType | filesystem | oscillates ||---|---|---|---|
| mapped SMB share | 4 (network) | NTFS | yes |
| internal disk | 3 (local) | NTFS | yes |
| cloud-sync client mount | 3 (local) | FAT32 | yes |
The heaviest oscillation I recorded is on a plain local NTFS drive
(
s:\dev\repos\repo-a373 records vsS:\dev\repos\repo-a2348), not on thenetwork mount. Worth noting because the mapped-drive framing in those issues may
be incidental — the reporters happened to be on mapped drives, but the casing
divergence does not require one.
Scale, on one machine. 4 of 12 keys in
~/.claude.jsonprojectsarecase-collision duplicates — 8 of 12 entries are two halves of one folder:
(Paths anonymized; drive letters, casing and depth preserved exactly.)
Longevity. Casing per session from a local session log, 2026-03 to 2026-07:
First lowercase session on record: 2026-04-06. Six distinct workspaces recorded
under both casings.
One symptom worth adding: besides the trust/permission drops described here,
this also hides session history. Transcripts for both identities land in the
same on-disk directory (Windows collapses the
w--/W--slugs), so thetranscript is present and correctly titled in the very directory the picker
reads — but isn't offered, because the lookup is scoped by the case-sensitive
project key. That's #62288, same root cause.
Field-level divergence on the pair above:
| field |
W:/|w:/||---|---|---|
|
hasTrustDialogAccepted|True|False||
hasCompletedProjectOnboarding|True| absent ||
projectOnboardingSeenCount|1|0|| all 16
last*telemetry fields | present | absent |One negative result, for completeness:
allowedToolsandmcpServerswereempty on both entries in my config, so I could not reproduce the MCP/
permission divergence from #67749 and #76994 here. Structurally possible, not
observed in my case.
Possible additional consequence of this same root cause: when the untrusted/wrong-cased project entry is hit in headless mode (claude -p, e.g. via Windows Task Scheduler), the effect isn't just dropped permissions.allow entries — MCP servers configured in the project's .mcp.json silently fail to load too, with no error, just missing tools. Reproduced consistently across two days of testing (local npx-spawned server + one HTTP server never loaded headlessly, despite working every time interactively from the same machine/config). See #63350 for the symptom from that angle — this issue looks like it may be the actual root cause.
Reproduced on Windows 11 + VS Code 1.131.0 + extension 2.1.220. I traced the extension-side
origin of the lowercase drive, which I think adds a second, independently fixable choke point
alongside the CLI-side
doe()fix proposed here.The extension does not pass
fsPathstraight through — it launders it throughfs.realpathSync, which is what silently preserves the lowercase drive.This issue notes the extension spawns with
cwd: workspaceFolder.uri.fsPath. That's right inspirit, but the actual derivation in
extension.js(2.1.220) is three structurally identicalsites:
The
realpathSynccall looks like it should canonicalize, and that may be why this has beeneasy to miss — but on Windows it does not touch drive-letter case. Only
fs.realpathSync.nativedoes, because it delegates to the OS canonicalization API. Measuredon Node v26.5.1:
ois then assigned tothis.cwdon the session class and fans out to everycwd: this.cwdconsumer, including the process spawn. So one unnormalized value propagatesto the CLI, which then keys
.claude.jsonoff it.One-line fix, at all three sites:
Error semantics are unchanged — both variants throw
ENOENTon a missing path — and.normalize("NFC")still applies. As a bonus,.nativecanonicalizes every path component,not just the drive.
Verified end to end. After patching the installed bundle and fully restarting VS Code (a
window reload is not enough — the extension host caches
extension.js), a fresh session with"entrypoint":"claude-vscode"recorded"cwd":"C:\\Users\\...", and no lowercase project keywas recreated when the CLI next rewrote
~/.claude.json. Before the patch, that samepanel-launched flow reliably produced the duplicate
c:/...key. As a side effect,ConvertFrom-Jsoncan read~/.claude.jsonagain — with both keys present, PowerShell failsoutright with
DuplicateKeysInJsonString, which breaks any user script that reads the config.On @normancates' oscillation finding (both
w:\andW:\within a single session): threeseparate derivation sites in the extension may help explain it. They are reached by different
entry paths (
resolveWebviewView,setupPanel, and a third panel-restore path), so which onecomputes
cwddepends on how a given view or panel was created or restored. Anything thatderives cwd outside these three sites would bypass the
realpathSyncstep entirely and keepwhatever casing it started with. Worth checking whether the non-
realpathSyncpaths are theuppercase ones.
Note this fix is complementary rather than an alternative to canonicalizing inside
doe().The extension fix stops one producer of bad input; the
doe()fix makes the CLI robust toany producer, including
claudeinvoked directly with an odd-cased cwd. Both seem worthdoing, and neither repairs config that is already split — an affected user still has two
project records on disk, so a one-time merge migration would be needed to fully close this out.