claude-code - 💡(How to fix) Fix /resume returns empty picker when launch-cwd encodes differently than original session cwd (path-encoding nondeterminism in fh()) [2 comments, 3 participants]

Official PRs (…)
ON THIS PAGE

Recommended Tools

×6

Utilities matched from this issue’s tags and category — try them while you read without losing context.

GitHub issue graph ai analysis

Paste a GitHub issue URL. We fetch that issue, discover linked issues from bodies/comments/timeline, collect linked pull requests, and produce a structured English report.

The report is written in English Markdown for sharing and archival.

Helpful · Quick feedback

Loading…
GitHub stats
anthropics/claude-code#54865Fetched 2026-04-30 06:33:44
View on GitHub
Comments
2
Participants
3
Timeline
6
Reactions
0
Timeline (top)
labeled ×4commented ×2

/resume (and the underlying gt(worktreePaths) function in cli.js) returns "No conversations found to resume" when Claude Code is launched from a cwd whose fh() encoding doesn't match the directory name under ~/.claude/projects/ where the session history was originally written.

The root cause is that fh() (the path-to-projects-dir-name encoder) is a plain non-alphanumeric → - regex replace with no canonical-form normalization. Equivalent representations of the same path produce different encoded directory names, so on Windows in particular, conversation history written from one path-form is invisible from another.

Root Cause

On Windows + MSYS2 (a very common Claude Code dev environment), the cwd reported by the shell shifts between /u/..., U:/..., U:\..., and //host/share/... constantly depending on how the user cd'd in, whether the project sits on a network share, and which terminal launched the process. Each form encodes to a different ~/.claude/projects/<dir>/ name, so users routinely "lose" entire conversation histories that are still on disk under the original encoding.

The widen-toggle in the picker (O ? await lhA() : ...) does work as a workaround once you find it, and claude --resume <session-id> works if you know the ID. But the silent-empty-picker is the dangerous failure mode: users believe their conversations are gone.

Fix Action

Fix / Workaround

The widen-toggle in the picker (O ? await lhA() : ...) does work as a workaround once you find it, and claude --resume <session-id> works if you know the ID. But the silent-empty-picker is the dangerous failure mode: users believe their conversations are gone.

Workarounds (for now)

Code Example

# In MSYS2 / Git Bash on Windows, in a project that has prior conversation history:
cd /u/some/project       # MSYS2-style path
claude
> /resume                # → "No conversations found to resume"

# Exit, then:
cd "U:\some\project"     # Windows cmd from same MSYS2 shell, OR launch claude from cmd.exe
claude
> /resume                # → conversations now visible

---

function fh(A) { return A.replace(/[^a-zA-Z0-9]/g, "-") }
function dp() { return gN(LQ(), "projects") }                  // ~/.claude/projects
function uH(A) { return gN(dp(), fh(A)) }                      // ~/.claude/projects/<fh(A)>

---

async function gt(A, Q) {
  let B = SA(), G = dp(), Z = await dH9();
  if (A.length <= 1) {
    if (Z) { let X = uH(FQ()); return RH9(X) }                 // ← uses fh(FQ()) — cwd at launch
    return h6A(Q)
  }
  try { B.statSync(G) } catch { return h6A(Q) }
  let Y = A.map(X => fh(X)), J = [];                            // ← encodes each worktree path
  try {
    let X = B.readdirSync(G);
    for (let I of X) {
      if (!I.isDirectory()) continue;
      let D = I.name;
      if (Y.some(K => D === K || D.startsWith(K + "-"))) J.push(gN(G, D))
    }
  } catch { return h6A(Q) }
  if (J.length === 0) return h6A(Q);
  // ...
}

---

let M = O ? await lhA() : await gt(L);
if (M.length === 0) { A("No conversations found to resume"); return }
RAW_BUFFERClick to expand / collapse

Summary

/resume (and the underlying gt(worktreePaths) function in cli.js) returns "No conversations found to resume" when Claude Code is launched from a cwd whose fh() encoding doesn't match the directory name under ~/.claude/projects/ where the session history was originally written.

The root cause is that fh() (the path-to-projects-dir-name encoder) is a plain non-alphanumeric → - regex replace with no canonical-form normalization. Equivalent representations of the same path produce different encoded directory names, so on Windows in particular, conversation history written from one path-form is invisible from another.

CLI Version

claude --version2.1.114 (Claude Code) on Windows 10 Pro 19045, MSYS2 / Git Bash shell.

Reproduction

The bug surfaces from at least three different launch-cwd permutations on Windows:

Launch CWD (as reported by pwd)fh(cwd)Project dir under ~/.claude/projects/ exists?
U:\0_Projects\SmartFormers-Business\…\Tools (Windows cmd, drive letter, backslash)U--0-Projects-SmartFormers-Business----ToolsOriginal session-write encoding — exists ✓
U:/0_Projects/SmartFormers-Business/…/Tools (forward slash, drive letter)U--0-Projects-SmartFormers-Business----ToolsSame as above ✓
/u/0_Projects/SmartFormers-Business/…/Tools (MSYS2, lowercase, no drive colon)-u-0-Projects-SmartFormers-Business----ToolsNo such directory ✗ → empty picker
//kaspar/Development/0_Projects/SmartFormers-Business (UNC path, same volume)--kaspar-Development-0-Projects-SmartFormers-BusinessNo such directory ✗ → empty picker

Add a git worktree and the picker takes a different code-path (A.length > 1 branch in gt()) which iterates ~/.claude/projects/ looking for entries that exactly match fh(worktreePath) for any of the worktrees. Same root cause, different symptom: even when conversation history exists, no worktree's encoded form matches a real directory, so the J array stays empty and the picker shows nothing.

Concrete test

# In MSYS2 / Git Bash on Windows, in a project that has prior conversation history:
cd /u/some/project       # MSYS2-style path
claude
> /resume                # → "No conversations found to resume"

# Exit, then:
cd "U:\some\project"     # Windows cmd from same MSYS2 shell, OR launch claude from cmd.exe
claude
> /resume                # → conversations now visible

Source

In cli.js (v2.1.114, ~5127 lines):

function fh(A) { return A.replace(/[^a-zA-Z0-9]/g, "-") }
function dp() { return gN(LQ(), "projects") }                  // ~/.claude/projects
function uH(A) { return gN(dp(), fh(A)) }                      // ~/.claude/projects/<fh(A)>
async function gt(A, Q) {
  let B = SA(), G = dp(), Z = await dH9();
  if (A.length <= 1) {
    if (Z) { let X = uH(FQ()); return RH9(X) }                 // ← uses fh(FQ()) — cwd at launch
    return h6A(Q)
  }
  try { B.statSync(G) } catch { return h6A(Q) }
  let Y = A.map(X => fh(X)), J = [];                            // ← encodes each worktree path
  try {
    let X = B.readdirSync(G);
    for (let I of X) {
      if (!I.isDirectory()) continue;
      let D = I.name;
      if (Y.some(K => D === K || D.startsWith(K + "-"))) J.push(gN(G, D))
    }
  } catch { return h6A(Q) }
  if (J.length === 0) return h6A(Q);
  // ...
}

The picker UI's branch:

let M = O ? await lhA() : await gt(L);
if (M.length === 0) { A("No conversations found to resume"); return }

Why this matters

On Windows + MSYS2 (a very common Claude Code dev environment), the cwd reported by the shell shifts between /u/..., U:/..., U:\..., and //host/share/... constantly depending on how the user cd'd in, whether the project sits on a network share, and which terminal launched the process. Each form encodes to a different ~/.claude/projects/<dir>/ name, so users routinely "lose" entire conversation histories that are still on disk under the original encoding.

The widen-toggle in the picker (O ? await lhA() : ...) does work as a workaround once you find it, and claude --resume <session-id> works if you know the ID. But the silent-empty-picker is the dangerous failure mode: users believe their conversations are gone.

Suggested fix

  1. Canonicalize before encoding in fh() callers — normalize Windows paths to a single form (resolve /u/U:, normalize separators, resolve UNC ↔ drive-letter for the same volume) before applying the non-alphanumeric → - regex.
  2. Or: scan all subdirectories of ~/.claude/projects/ and match against the canonical form of the cwd, rather than relying on the encoded form being a stable directory name.
  3. At minimum: log a warning when the picker returns empty but there are sibling directories under ~/.claude/projects/ that resolve to the same canonical path as the current cwd. ("Did you mean: U--0-Projects-... (87 conversations)?")

Workarounds (for now)

  • claude --resume <session-id> — direct resume by ID
  • Press the widen toggle in the picker UI to switch to lhA() (all projects, unfiltered)
  • Always launch Claude from the same path-form (e.g., always U:\..., never /u/...)

Related

  • This was originally surfaced as a "stale git worktree breaks /resume" bug, but post-cleanup investigation (full source dive into gt/fh/dp/uH) showed worktrees are not the root cause — they only widen the surface area. The same empty-picker reproduces on single-worktree state when the launch-cwd path-form differs from history-write path-form.

extent analysis

TL;DR

The most likely fix is to canonicalize Windows paths to a single form before encoding in the fh() function to ensure consistent directory names under ~/.claude/projects/.

Guidance

  1. Canonicalize paths: Normalize Windows paths to a single form (e.g., resolve /u/U:, normalize separators, resolve UNC ↔ drive-letter for the same volume) before applying the non-alphanumeric → - regex in fh().
  2. Verify canonicalization: Test the canonicalization process with different path forms (e.g., /u/, U:, U:\, //host/share/) to ensure they resolve to the same encoded directory name.
  3. Update fh() function: Modify the fh() function to include the canonicalization step before encoding the path.
  4. Test the fix: Verify that the picker UI correctly displays conversation history after implementing the canonicalization fix.

Example

function fh(A) {
  // Canonicalize Windows paths
  A = A.replace(/\/u\//, 'U:'); // Resolve /u/ to U:
  A = A.replace(/\\/g, '/'); // Normalize separators
  // ... other canonicalization steps ...
  return A.replace(/[^a-zA-Z0-9]/g, "-"); // Apply non-alphanumeric → - regex
}

Notes

The provided code snippet is a simplified example and may require additional canonicalization steps depending on the specific requirements.

Recommendation

Apply the workaround of canonicalizing paths in the fh() function to ensure consistent directory names under ~/.claude/projects/. This fix addresses the root cause of the issue and provides a reliable solution.

Vote matrix · Quick signals

Works
Did the solution work? Tap to confirm.
Easy Fix
Was it a quick fix?
Time Saver
Did it save you time?
Blocking
Was it severely blocking?
Common Issue
Are others likely hitting this too?
Flaky / Intermittent
Is it intermittent?
Verified / Reproducible
Can you reproduce it reliably?
Loading…

Still need to ship something?

×6

Another batch ranked right after the header list — different links, same matching logic.

Back to top recommendations

TRENDING

claude-code - 💡(How to fix) Fix /resume returns empty picker when launch-cwd encodes differently than original session cwd (path-encoding nondeterminism in fh()) [2 comments, 3 participants]