[Windows] /resume / session history always empty for projects on mapped network drives — extension and CLI compute different project slugs

Status Closed — duplicate
Reported on v2.1.205
Maintainer reply None cached
Activity 6 comments · opened Jul 9, 2026 · closed Aug 19, 2026

Environment

  • Claude Code VSCode extension: v2.1.205 (native binary claude.exe 2.1.205, win32-x64)
  • OS: Windows 11 Pro (10.0.26200)
  • Project location: mapped SMB network drive (Y:\\server\share), workspace opened as Y:\my-project

Summary

When a project lives on a mapped network drive, the VSCode panel's session history (/resume, past chats) is always empty — only the live session appears — even though all transcripts exist and are perfectly valid. Users understandably conclude their history is lost. It is not: the extension is simply looking in the wrong directory.

Root cause

The CLI and the extension canonicalize the project path differently before slugifying it into ~/.claude/projects/<slug>:

  • The CLI writes transcripts under the slug of the raw cwd:

y:\my-project~/.claude/projects/y--my-project/ ✅ (all sessions are here)

  • The extension's session listing first resolves the cwd with fs.promises.realpath(cwd). In Node, fs.promises.realpath is the native implementation (libuv uv_fs_realpathGetFinalPathNameByHandle on Windows), which resolves mapped drives to their UNC target — unlike fs.realpathSync/fs.realpath (JS implementation), which keeps the drive letter:

``
> fs.realpathSync('y:/my-project') // JS impl
y:\my-project
> await fs.promises.realpath('y:/my-project') // native impl
\\server\share\my-project
``

The extension therefore looks for sessions in
~/.claude/projects/--server-share-my-project/ — a directory that does not existreaddir throws → empty session list.

The mismatch is deterministic: reader and writer can never agree for any workspace on a mapped drive, so history appears permanently empty on every launch.

I verified the rest of the pipeline is healthy by replaying the picker's filtering logic (head/tail 64 KB window, entrypoint/sessionKind programmatic filter, isSidechain first-line check, title extraction) against all 19 transcripts of the affected project: 19/19 pass. Only the directory resolution differs.

Steps to reproduce

  1. Map a network share: net use Y: \\server\share
  2. Open Y:\my-project in VSCode with the Claude Code extension and run a couple of sessions.
  3. Observe transcripts being created in %USERPROFILE%\.claude\projects\y--my-project\.
  4. Type /resume (or open past chats) in the panel.

Expected: past sessions of the workspace are listed.
Actual: the list is empty (only the currently live session is shown). Terminal claude --resume from Y:\my-project works fine, since the CLI uses the drive-letter slug.

Workaround

An NTFS junction making both slugs point at the same directory:

New-Item -ItemType Junction `
  -Path "$HOME\.claude\projects\--server-share-my-project" `
  -Target "$HOME\.claude\projects\y--my-project"

/resume immediately lists every past session again.

Suggested fix

Use the same canonicalization on the write and read paths. Either avoid the native realpath when deriving the project slug (matching the CLI), or fall back to probing the raw-cwd slug when the realpath-derived slug directory doesn't exist. (Handling both slugs on read would also repair history for users who already have transcripts split across the two forms.)

View original on GitHub ↗

4 Comments

BasedGPT · 1 month ago

All 19 sessions are under y--my-project/. The write path is correct; the extension's native-realpath resolver is reading from the wrong slug, which explains the clean 19/19 pass when you replay the filtering logic against the right directory.

This is the split-slug pattern I built BasedGPT/claude-code-session-recovery to handle: two slug directories where only one has content, and the reader landing on the empty one. In the toolkit it maps to the one_project_two_session_sets case, though usually triggered by a folder rename or junction rather than a drive-letter to UNC resolution divergence.

The junction workaround you found is exactly what I'd recommend. Creating --server-share-my-project as a junction targeting y--my-project gets the extension pointing at the right transcript set without touching the files. Run diagnose.py from the toolkit first. It maps %APPDATA%\Claude\claude-code-sessions\ against ~/.claude/projects/ and shows both slug directories, so you can confirm the mismatch before creating the junction.

The fix you've outlined (fall back to the raw-cwd slug when the realpath-derived directory doesn't exist) is the right upstream call. The second option (handle both slug forms on read) would also repair existing splits for users who already have transcripts under both.

Hope this helps, if my tools are able to help you, would appreciate a ⭐ :)

TPT-jmasa · 1 month ago

Confirming this is still present in extension v2.1.210 (Windows 11 Pro 10.0.26200, workspace on a mapped SMB drive). Same root cause verified independently: CLI writes transcripts under the drive-letter slug, while the extension resolves the cwd with native fs.promises.realpath() (UNC path) and scans a slug that does not exist.

Two additions to the write-up above:

  1. It is not only the picker: restored chat tabs also come up blank after a VSCode restart, because the per-session transcript lookup on webview reload goes through the same UNC-resolving canonicalization and finds no file. The visible symptom is history list empty and previously-open chats losing their content, so users conclude their data is gone (all .jsonl files are intact under the drive-letter slug).
  1. The NTFS junction workaround works on 2.1.210 as well — after creating junctions for all 15 affected projects on the mapped drive, the exact readdir the extension performs returns every session. The junction must be re-created for each new project on the mapped drive, so a real fix (same canonicalization on read and write paths) would be much appreciated.
normancates · 1 month ago

I realise the essentials of the below are already mentioned by the OP and later posters.

But this is a worked confirmation PLUS the cost of lost time and frustration to us
---------------------------

Confirming this on Windows 11 with a full mechanism, a regression window, a
same-machine control, and a working workaround.

Setup: workspace on a mapped SMB drive (W: -> \\NAS\workspace), VS Code
extension 2.1.214, CLI 2.1.215. Other workspaces on local drives, same machine,
same VS Code instance.

Mechanism

The extension and the CLI derive different project directory names for the same
workspace, because one resolves the mapped drive letter to its UNC target and the
other does not. Running each through the slug function in the shipped
extension.js:

function mx(e){ return e.replace(/[^a-zA-Z0-9]/g, "-") }
CLI:        w:\                          ->  w--
Extension:  \\NAS\workspace              ->  --NAS-workspace

The CLI writes transcripts to ~/.claude/projects/w--/. The extension looks in
~/.claude/projects/--NAS-workspace/, which has never existed, finds nothing,
and returns an empty list.

Confirmed by workaround

Creating the UNC-derived directory as a junction to the real one immediately
restored the full session list in the VS Code panel:

cd %USERPROFILE%\.claude\projects
mklink /J "--NAS-workspace" "w--"

After this, both surfaces work and stay unified — a new chat started in VS Code
wrote its transcript through the junction into w--, alongside the CLI's. No
duplicate directory was created.

That the workaround works is the proof of the mechanism: nothing else changed.

Same-machine control

In the same VS Code instance, workspaces on local drives list their sessions
correctly (E:, S:). Only the mapped network drive is affected. Local drives
have no UNC target, so both surfaces derive the same slug.

Regression window

This is a regression, not long-standing behaviour:

Extension 2.1.214   installed   18 Jul, 15:29
Session A           created     18 Jul, 05:04-10:55   (before the update)
Session A missing from picker   19 Jul                (after the update)

The transcript never moved; the lookup path did. The workspace had been working
normally for months prior.

The log makes this undiagnosable

This is the part worth fixing regardless of the path bug. The extension log shows
the request going out and simply never being answered — no result, no error, no
indication of what it looked for:

16:32:22.774 [info] Received message from webview: {"request":{"type":"list_sessions_request"}}
16:32:23.427 [info] Received message from webview: {"request":{"type":"list_sessions_request"}}
16:32:24.479 [info] Received message from webview: {"request":{"type":"list_remote_sessions"}}
16:32:25.436 [info] Fetched 0 remote sessions
16:32:26.338 [info] Received message from webview: {"request":{"type":"list_sessions_request"}}

list_remote_sessions reports its result. list_sessions_request never does.

Logging the resolved project directory once per lookup would have reduced this
from a multi-hour investigation to about thirty seconds.
The single line
looking for sessions in ~/.claude/projects/--NAS-workspace names the bug
outright.

Cost of diagnosing this

Roughly four hours of one working day, by a user who had already lost time to
the same underlying class of problem earlier that morning. That included:

  • Seven incorrect hypotheses, each requiring a VS Code restart to test
  • A Claude Code session restart (which itself lost the session from the picker)
  • A NAS reboot
  • Reading the minified extension.js to find the slug function
  • Three other issues filed upstream that morning for related case-sensitivity

symptoms, before the actual cause here was identified

None of the obvious remedies helped, and each cost a test cycle: merging the
duplicate case-variant keys in ~/.claude.json, renaming the project directory
to match the extension's expected case, setting hasTrustDialogAccepted,
matching CLI and extension versions, clearing orphaned session directories.

The failure presents as "all my history is gone" with no error anywhere in the
UI, so the natural user response is to assume data loss and start trying to
recover it. Every recovery attempt is wasted, because the transcripts were intact
on disk the entire time and claude --resume <id> worked throughout.

Suggested fixes, in order of value

  1. Log the resolved project directory on every session-list lookup. This is a

one-line change that makes the entire bug class self-diagnosing.

  1. Derive the project slug from the same path form in both surfaces — either

both resolve to UNC, or neither does. Any consistent choice fixes this.

  1. Surface an empty result distinguishably. "No sessions found in <path>" is

very different from "no sessions", and only the first is actionable.

  1. If a lookup path does not exist, consider falling back to the unresolved form

before returning empty — that alone would have made this invisible to users.

Related

The same unnormalized-path-as-key pattern shows up across
#75855, #76994, #62288, #67749, #77837, #74912. This issue is the mapped-network-
drive variant. On the same machine, #77837 was also live: workspace trust granted
from the CLI as W:/ did not apply to the extension's w:/, silently voiding 9
permissions.allow rules with only a debug-log mention.

Tuxprogrammer · 22 days ago

Confirming this is still present in extension v2.1.226 (Windows 11 Pro 10.0.26200, VS Code 1.132.0, workspace on a mapped SMB drive Z:\\fileserver\share).

Adding the exact minified call chain in the shipped extension.js, since the identifiers have changed since the earlier reports (the slug function is yF in 2.1.226, not mx). All snippets below are verbatim from ~/.vscode/extensions/anthropic.claude-code-2.1.226-win32-x64/extension.js.

The divergence is inside a single dispatch function

tde() is the one entry point for session enumeration, and its two branches canonicalize the cwd differently:

async function tde(e){if(e?.sessionStore)return p$t(e.sessionStore,e);return PAt(e)}

Branch A — sessionStore (drive-letter-preserving). Uses rde(), which calls the JS realpathSync:

function rde(e){let t=Qt.resolve(e??"."),r;try{r=cz.realpathSync(t)}catch{r=t}return nu(r)}
function Cx(e){return $h(rde(e))}

async function p$t(e,t){let r=rde(t.dir),n=$h(r), ... }

Branch B — filesystem (PAtRAt, UNC-expanding). Uses ble(), which calls the native-backed fs.promises.realpath:

async function ble(e){try{return nu(await ao.realpath(e))}catch{return nu(e)}}

async function RAt(e,t,r){let n=await ble(e),i;
  if(t)try{i=await fle(n)}catch{i=[]}else i=[];
  if(i.length<=1){let d=[];for(let p of await Db(n))d.push(...await Cb(p,r,n));return d}
  ... }

So the same bundle contains two canonicalizers that can never agree on a mapped drive. Every projectKey producer in the session-store path goes through Cx() (drive letter):

async function h$t(e,t,r){let n=Cx(r),i=await e.load({projectKey:n,sessionId:t}); ... }
async function g$t(e,t,r){ ... let n=Cx(r.dir),i=await e.load({projectKey:n,sessionId:t}); ... }
async function Jue(e,t,r,n,i=60000,o){ ... let s=Cx(r),a=await iu(e.load({projectKey:s,sessionId:t}), ... ) }

Where the failure is swallowed

Db() is the only place the derived directory is touched, and its readdir failure is caught and discarded, so the caller sees an ordinary empty list rather than an error:

async function Db(e,t){if(t)return sAt(e,t);
  let r=yle(e),n=[];
  try{await ao.readdir(r),n.push(r)}catch{}      // <-- ENOENT swallowed, returns []
  let i=$h(e);if(i.length<=lu)return n;
  ... }

with the slug/path helpers:

function yF(e){return e.replace(/[^a-zA-Z0-9]/g,"-")}
function $h(e){let t=yF(e);if(t.length<=lu)return t;return`${t.slice(0,lu)}-${oAt(e)}`}
function Uh(){return ss.join(ah(),"projects")}
function yle(e){return ss.join(Uh(),$h(e))}

Entry point from the RPC handler:

async listSessions(){let e=await tde({dir:this.cwd,includeWorktrees:!1,includeProgrammatic:this.includeProgrammaticSessions}), ... }

Full chain: listSessions()tde()PAt()RAt()ble() (UNC)Db() → ENOENT → [].

Minimal reproduction, no Claude Code involved

The underlying Node divergence, on Node v24.11.0 with Z: mapped to \\fileserver\share:

fs.realpathSync('z:\\myproject')          // 'z:\\myproject'          <- JS impl, used by rde()
fs.realpathSync.native('z:\\myproject')   // '\\\\fileserver\\share\\myproject'
await fs.promises.realpath('z:\\myproject') // '\\\\fileserver\\share\\myproject'  <- used by ble()

Feeding both through yF():

rde -> z--myproject                      -> ~/.claude/projects/z--myproject          (exists, 3 .jsonl)
ble -> --fileserver-share-myproject      -> ~/.claude/projects/--fileserver-share-myproject  (ENOENT)

Everything downstream is healthy

I reimplemented the rest of the picker against the real transcripts to rule out a second cause. All of it passes; the directory resolution is the only fault:

  • getHiddenSessionIds()hiddenSessionIds is absent from the extension's VS Code globalState, so the filter in listSessions() is inert.
  • Programmatic filter — excluded only when includeProgrammatic is false, but the caller passes includeProgrammaticSessions hardcoded !0.
  • Worktree / current-workspace filter — Ljt(e,t){let r=FB(t);if(!r)return!0; ... } with Djt=/[/\\]\.claude[/\\]worktrees[/\\]([^/\\]+)$/. A non-worktree cwd makes FB(t) undefined, so this returns !0 for every session.
  • Metadata parser Sle() / head-tail reader vle() ($c = 65536) — replayed against every transcript in the project; none returns null, and cwd, title and isSidechain all extract correctly.

Replaying RAt → Db → Cb → Cle verbatim returns 0 sessions before the junction and all 3 after, with no other change.

One symptom worth calling out for triage

The currently-open session still appears in the panel, because the webview holds it as the live session object rather than as a result of the query. So the list looks like it has "one recent session" instead of looking broken — which reads as a stale cache and sends people toward Developer: Reload Window, which of course does nothing. That misdirection is probably why this keeps getting rediagnosed.

Suggested fix

Point ble() at the same canonicalization rde() already uses, so both branches of tde() agree. Failing that, have Db() also probe the Cx()/raw-cwd slug when the ble()-derived directory is missing — that second form additionally repairs history for users who already have transcripts split across both slugs.

The NTFS junction workaround from the original report still works on 2.1.226. It has to be recreated per project per mapped drive, which gets tedious with several mapped drives on one machine.

Showing cached comments. Read the full discussion on GitHub ↗