/resume returns empty picker when launch-cwd encodes differently than original session cwd (path-encoding nondeterminism in fh())

Resolved 💬 5 comments Opened Apr 30, 2026 by CreativeWarlock Closed Jun 13, 2026

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----Tools | Original session-write encoding — exists ✓ |
| U:/0_Projects/SmartFormers-Business/…/Tools (forward slash, drive letter) | U--0-Projects-SmartFormers-Business----Tools | Same as above ✓ |
| /u/0_Projects/SmartFormers-Business/…/Tools (MSYS2, lowercase, no drive colon) | -u-0-Projects-SmartFormers-Business----Tools | No such directory ✗ → empty picker |
| //kaspar/Development/0_Projects/SmartFormers-Business (UNC path, same volume) | --kaspar-Development-0-Projects-SmartFormers-Business | No 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.

View original on GitHub ↗

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