Session becomes inaccessible via --resume / desktop UI despite intact .jsonl transcript (stale session index after directory path change)

Status Open
Maintainer reply None cached
Activity 3 comments · opened Jul 4, 2026

This report was drafted by Claude Code (AI assistant) based on debugging done during my session, and filed on my behalf with my review/approval.

---

Over several sessions working in a project directory, I had three prior Claude Code sessions become inaccessible — they didn't show up as resumable via --resume, nor could I reopen them through the desktop app's session picker.

I confirmed this is not data loss: the raw .jsonl transcript files for each of the affected sessions still exist on disk, fully intact, under ~/.claude/projects/<project-slug>/, with normal size and content matching the work actually done.

Suspected cause: the project's working directory was originally accessed via a symlinked path, then later accessed via the real resolved path (or vice versa). Claude Code appears to key its session index off of process.cwd()/getcwd()-style resolution (which always resolves through symlinks to the physical path), while at some point the session may have been recorded or looked up under the logical/symlinked path instead. The two path forms don't match in the session index, so lookups by --resume (and the desktop app's picker) fail to find sessions that are demonstrably present in ~/.claude/projects/.

I ruled out other causes I could think of (e.g., permissions, corruption, project-slug hashing changes unrelated to symlinks) — the .jsonl files decode fine and match expected content; only session discovery seems to be broken.

Impact: Users working through very long sessions (e.g., an all-day multi-issue session) can lose the ability to resume or browse a session's history through normal means, even though nothing was actually lost, which is confusing and makes it look like data loss when it isn't.

Environment: macOS (Darwin 25.3.0), Claude Code CLI, project directory previously accessed via a symlink that was later resolved to its real path (or the reverse ordering).

View original on GitHub ↗

3 Comments

yurukusa · 1 month ago

Confirmed your diagnosis first-hand — the symlinked-path mismatch is exactly it. Here's the divergence that causes it, plus how to get the "lost" sessions back (nothing is actually gone).

Verified mechanism

Session directories under ~/.claude/projects/ are named by taking the project's cwd and replacing every / with -. Directly observed:

/home/namakusa/projects/cc-loop  ->  -home-namakusa-projects-cc-loop
/tmp                             ->  -tmp

The trap: a symlinked working directory resolves to two different absolute paths depending on which call you use, so it yields two different slugs. Reproduced on Linux (POSIX getcwd() resolves symlinks the same way on macOS):

$ ln -s .../scratchpad/realproj .../scratchpad/linkproj
$ cd .../scratchpad/linkproj
$ pwd -P                     # physical — what getcwd()/os.getcwd() returns
.../scratchpad/realproj
$ echo "$PWD"                # logical — the symlinked path
.../scratchpad/linkproj
python: os.getcwd()          -> .../scratchpad/realproj
python: os.environ['PWD']    -> .../scratchpad/linkproj

So if a session is recorded under one resolution (e.g. logical $PWD = -…-linkproj) but --resume / the picker looks up under the other (getcwd() = -…-realproj), the two slug directories don't match and discovery fails — while the .jsonl sits there intact under the first slug. That matches your "intact transcript, broken discovery" symptom precisely.

Recovering the sessions (no data loss)

  1. Find where the orphaned transcripts actually live — you'll likely see two dirs, one per path form:

``
ls -la ~/.claude/projects/ | grep <project-name>
``

  1. Then either:
  • relaunch Claude Code from the same path form used when those sessions were created (so the current getcwd() slug matches where they're stored), or
  • move the intact transcripts from the orphaned slug dir into the slug dir matching your current resolved path:

``
mv ~/.claude/projects/<orphan-slug>/*.jsonl ~/.claude/projects/<current-slug>/
`
(copy first if unsure). After that,
--resume` and the picker find them again.

A cheap guard until it's fixed upstream

A SessionStart hook that flags a symlinked launch dir catches this before a session goes missing:

#!/usr/bin/env bash
# warn if cwd is reached through a symlink (sessions may land under a different project slug)
if [ "$(pwd -L)" != "$(pwd -P)" ]; then
  echo "⚠ cwd is symlinked ($(pwd -L) -> $(pwd -P)); sessions may be recorded under a different project slug and not appear in --resume." >&2
fi
exit 0

Real fix is on the CC side: resolve the project path the same way for both recording and lookup — canonical realpath for both is the safe choice — or store the canonical path in the session index and match against it. Until then, the record-vs-lookup asymmetry will keep stranding sessions on symlinked project dirs.

(I verified the slug derivation and the getcwd()-vs-$PWD divergence directly. I did not reproduce CC's internal record-vs-lookup path — that part is inferred from your evidence plus the divergence above, and it fits the symptom.)
</content>

BasedGPT · 16 days ago

The intact JSONL files and two path-derived project slugs make this a recoverable path-split case rather than lost conversation data.

I built BasedGPT/claude-code-session-recovery for this. On the affected machine, run:

python tools/diagnose.py

Follow the exact command it prints. If it identifies two project slugs or a changed working-directory path, review the printed dry run, keep copies of the metadata and transcript directories, then fully quit Desktop before applying it. The guarded repair reconnects metadata to the project slug containing each transcript and leaves the JSONL files in place.

That is safer than moving transcripts manually, and it preserves the original files if the path forms need further comparison. After applying, reopen Desktop and confirm the sessions appear in the session list before changing either project directory.

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

shleder · 7 days ago

For the symlink or resolved-path discovery mismatch, please test with the explicit canonical Claude state root and report which transcripts are visible.

Thanks for the detailed report. This is a good candidate for a bounded recovery check. Vetto 0.2.0-alpha.2 is available from npm and adds an experimental read-only Claude adapter. It treats Claude JSONL as opaque; it does not resume, rewrite, or reconstruct provider state. On a disposable copy, try: npm install --global @shleddy/vetto@next; vetto rescue --adapter claude --root <CLAUDE_STATE_ROOT> --json scan; then use the exact returned key with diagnose and snapshot ... --output ./vetto-recovery/session.jsonl. Please report OS, Claude/Vetto versions, sanitized JSON, and source SHA-256 before/after; never upload raw transcripts, settings, credentials, prompts, or tokens. An explicit unavailable/unsupported result is useful too.