claude-code - 💡(How to fix) Fix `/resume` accumulates "phantom sessions" — sessionId allocated + speculative ai-title written before picker shown, never reaped on cancel

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…

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.

Root Cause

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.

Fix Action

Fix / Workaround

Workaround for affected users

Code Example

{"type":"ai-title","aiTitle":"Viglink ETL 加速優化 Round 1 — Ozark 整合 Hoist","sessionId":"d5eb770f-..."}
{"type":"ai-title","aiTitle":"Viglink ETL 加速優化 Round 1 — Ozark 整合 Hoist","sessionId":"d5eb770f-..."}

---

grep -LE '"type":"(user|assistant|system)"' ~/.claude/projects/*/*.jsonl

---

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
RAW_BUFFERClick to expand / collapse

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

  1. From any project dir, run claude --resume (or /resume from inside a session). Picker UI appears.
  2. Press Esc / Cancel without selecting anything → "Resume cancelled".
  3. Check ~/.claude/projects/<encoded-cwd>/. A new 139-byte JSONL file appears with sessionId you've never seen.
  4. Contents: 2 ai-title entries with a title speculatively generated from your recent project activity. Zero user/assistant/system entries.

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)

  1. Lazy session allocation: /resume / /recap should open picker first, allocate sessionId only when user selects/types. No allocation = no orphan JSONL on cancel.

  2. Reap on cancel: on cancel path, delete the spawned-but-unused sessionId's JSONL.

  3. Hide empty sessions in picker: filter out sessions whose JSONL has only ai-title entries (no user/assistant/system). They have no conversation content — nothing to resume to.

  4. 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: /resume and /recap slash commands

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` accumulates "phantom sessions" — sessionId allocated + speculative ai-title written before picker shown, never reaped on cancel