/resume returns empty picker when launch-cwd encodes differently than original session cwd (path-encoding nondeterminism in fh())
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 --version → 2.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
- 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. - 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. - 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 worktreebreaks/resume" bug, but post-cleanup investigation (full source dive intogt/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.
This issue has 5 comments on GitHub. Read the full discussion on GitHub ↗