`/resume` accumulates "phantom sessions" — sessionId allocated + speculative ai-title written before picker shown, never reaped on cancel
Summary
Every invocation of /resume (and similar slash commands like /recap) spawns a brand-new session UUID and writes a speculatively-generated ai-title to its JSONL before the picker UI is shown. If the user cancels the picker, the spawn isn't rolled back — the JSONL persists with only an ai-title entry and zero conversation content.
These "phantom sessions" accumulate forever in ~/.claude/projects/<encoded-cwd>/. The picker lists them alongside real sessions under their speculative title, with no visual distinction. Real users mistake them for actual prior work and /resume into them, where Claude answers their domain-specific questions with empty context — a silent failure mode.
Reproducer
- From any project dir, run
claude --resume(or/resumefrom inside a session). Picker UI appears. - Press Esc /
Cancelwithout selecting anything → "Resume cancelled". - Check
~/.claude/projects/<encoded-cwd>/. A new 139-byte JSONL file appears with sessionId you've never seen. - Contents: 2
ai-titleentries with a title speculatively generated from your recent project activity. Zerouser/assistant/systementries.
Example phantom JSONL (139 bytes total):
{"type":"ai-title","aiTitle":"Viglink ETL 加速優化 Round 1 — Ozark 整合 Hoist","sessionId":"d5eb770f-..."}
{"type":"ai-title","aiTitle":"Viglink ETL 加速優化 Round 1 — Ozark 整合 Hoist","sessionId":"d5eb770f-..."}
Next time you run /resume, this phantom appears in the picker under "Viglink ETL 加速優化 Round 1 — Ozark 整合 Hoist · 139 bytes" alongside real sessions with similar titles.
Why this is a high-impact bug
Silent failure mode: user has been working on Viglink ETL for hours. They /resume, see "Viglink ETL 加速優化 Round 1 — Ozark 整合 Hoist" in picker, click. They type /recap or 幫我 rebase pr expecting Claude to recall their actual viglink work. Claude has empty context but answers plausibly from cwd inspection. User accepts the answer thinking it's informed by their actual prior session.
Observed empirically 2026-05-18 (today):
- 41 phantom sessions accumulated in
~/.claude/projects/-Users-gtso-IdeaProjects-affiliate-vampire-kb/ - Earliest phantom mtime: 2026-04-27
- User reported "I asked the resumed session about 'rebase pr', got an answer about kb main branch, then realized this isn't my actual viglink session — it's empty"
Suggested fixes (any one would resolve)
- Lazy session allocation:
/resume//recapshould open picker first, allocate sessionId only when user selects/types. No allocation = no orphan JSONL on cancel.
- Reap on cancel: on cancel path, delete the spawned-but-unused sessionId's JSONL.
- Hide empty sessions in picker: filter out sessions whose JSONL has only
ai-titleentries (nouser/assistant/system). They have no conversation content — nothing to resume to.
- Don't speculatively pre-write ai-title: generate ai-title only after first real user message. Then phantom JSONL would be empty file and easier to detect/filter at picker layer.
Workaround for affected users
Detect phantoms (single-grep, very fast across all projects):
grep -LE '"type":"(user|assistant|system)"' ~/.claude/projects/*/*.jsonl
Mark them with a visible prefix AND sink mtime so they don't crowd the picker:
PREFIX="👻 [PHANTOM] "
grep -LE '"type":"(user|assistant|system)"' ~/.claude/projects/*/*.jsonl 2>/dev/null | while read f; do
# 1. Mark title (idempotent)
jq -c --arg pfx "$PREFIX" '
if (.aiTitle | type) == "string" then
if (.aiTitle | startswith($pfx)) then .
else .aiTitle = $pfx + .aiTitle
end
else . end
' "$f" > "${f}.tmp" && mv "${f}.tmp" "$f"
done
# 2. Sink mtime so they sort to bottom (picker is mtime-sorted, top-N likely)
grep -LE '"type":"(user|assistant|system)"' ~/.claude/projects/*/*.jsonl 2>/dev/null \
| xargs touch -t 202401010000.00
After this two-step fix, phantoms still exist (resumable by UUID if ever needed) but don't crowd the picker top. Re-run after each /resume cancel cycle to handle new phantoms.
Important: marking with prefix alone is NOT enough — the jq write bumps each phantom's mtime to "now", pushing real older sessions OUT of picker's top-N window. Must also sink mtime.
Related bug
The other picker bug I filed: https://github.com/anthropics/claude-code/issues/59415 (sessionKind:"bg" taint from FleetView + /compact makes sessions disappear from picker). The phantom bug is the opposite failure: empty sessions APPEAR in picker pretending to be real. Both are picker-layer issues likely in the same display logic.
Environment
- Claude Code version: 2.1.142 → 2.1.143 (auto-updated during my session today; phantoms predate the update so this isn't a new regression)
- OS: macOS (Darwin 25.4.0)
- Shell: zsh
- Affected feature:
/resumeand/recapslash commands
This issue has 3 comments on GitHub. Read the full discussion on GitHub ↗