Session .jsonl rewritten to metadata-only stub (zero user/assistant records) at resume time; subagent transcripts survive — 27 sessions affected, 2.1.21x

Status Open
Reported on v2.1.215
Maintainer reply None cached
Activity 4 comments · opened Jul 20, 2026

This is the same signature as #18311 (main conversation lost while subagents/*.jsonl survive intact) — that issue is locked, so filing the variant separately with forensics that may help localize it. In our case the main .jsonl is not missing but rewritten down to a metadata-only stub, and the rewrite moment correlates exactly with resume/relaunch events.

Symptom

27 sessions in one project dir (~/.claude/projects/<proj>/) have main session files containing zero user/assistant records — only sidecar metadata (last-prompt, ai-title/custom-title, agent-name, agent-setting, mode, permission-mode, bridge-session) plus queue-operation records. Example stub is ~800 bytes to ~17KB. claude --resume <sid> on these opens an empty conversation. Meanwhile each session's subagents/*.jsonl files are fully intact (as in this issue), and tool-results/*.txt survive.

Forensics

  • The stub mtimes cluster exactly on resume/relaunch events. A batch of ~8 sessions all have mtimes 21:24–21:48 on 2026-07-18 — the window in which a fleet-recovery script relaunched them via claude --resume — minutes after auto-update installed 2.1.215 (binary mtime 21:32). Another session's stub mtime is the exact minute of a manual interactive /resume the next day.
  • Oldest affected session is from June 25; most are July. Long-running archived copies of one session show full ~27MB transcripts archived 2026-07-08 but 1KB stubs archived from the same session id on 2026-07-17 — so the behavior changed for us somewhere in the 2.1.20x→2.1.21x window.
  • Affected sessions' prompts are also absent from ~/.claude/history.jsonl, and cleanupPeriodDays is not set (default) — this is not the retention cleanup (#59248): the files exist, with fresh metadata, minus the messages.
  • The stubs' last-prompt/title records are correct for the lost conversation (the title and final prompt survive; a 200-char truncation applies to lastPrompt).

What does NOT reproduce it (tried on 2.1.215, Linux)

  • claude -p --resume <sid> on a small fresh 2.1.215-created session: appends correctly, history intact.
  • Interactive TUI claude --resume <sid> on the same session, one turn, /exit: intact.

So a trivial same-version resume round-trip is clean. The real losses involved long sessions (10h+, MBs, background tasks/queue-operations pending, subagents) written by 2.1.212/2.1.214 and resumed under 2.1.215. Cross-version resume of a trivial session (created by the 2.1.214 binary, TUI-resumed under 2.1.215, one turn, /exit): also intact. So the trigger is conditional on something beyond the version boundary alone — the affected sessions were all long-lived (10h+, MB-scale), had pending queue-operation records (background tasks/monitors), spawned subagents, and several were relaunched by scripts rather than interactively. Candidate interactions: the resume-hang-on-large-tool-outputs shape (#21067) followed by a user kill mid-rewrite, or the silent persist-stop-after-cd shape (#22566) having already emptied what resume later rewrote.

Environment

  • Linux (Liquorix 7.0.11), CLI 2.1.212 → 2.1.214 → 2.1.215 auto-updates on Jul 16/17/18
  • Anthropic subscription (Max), default retention settings
  • Sessions heavily used background tasks (queue-operation records present in every stub)

Happy to provide sanitized stub files or run instrumented repros — we have rolling backups in place now, so we can afford to sacrifice sessions to this.

View original on GitHub ↗

4 Comments

rsclafani · 1 month ago

Confirming this on Linux, and adding a second failure mode plus a measurement method that
may help separate signal from noise in the other reports.

Environment: Ubuntu aarch64, ext4 root, native installer, versions 2.1.215 → 2.1.217.
Many concurrent sessions, launched under a terminal multiplexer.

The stub form matches exactly

Same signature as described here: a .jsonl reduced to last-prompt, ai-title, mode,
permission-mode, system, file-history-snapshot, and zero user or assistant
records
, while <uuid>/subagents/*.jsonl and <uuid>/tool-results/* survive complete.

One example: a session that ran ~40 minutes and produced a real commit was reduced to 2,171
bytes, while its subagent transcript from the same session survived at 448 KB.

The resume correlation in this issue holds up. That stub's mtime is 20:03:29. My shell
history shows /resume issued in that project directory at 20:03. The session had been idle
for roughly eight hours before that. The resume appears to be the write that destroyed it.

A second form: the file is gone entirely

Alongside the stubs, sessions that ran real turns have no .jsonl at all anywhere under
~/.claude/projects/, while every sidecar survives: session-env/<uuid>/,
file-history/<uuid>/, and the sibling projects/<proj>/<uuid>/ directory with its
subagents/ and tool-results/ contents. claude --resume <uuid> returns
No conversation found with session ID: <uuid>.

This looks like the same failure as #78821, and I suspect the stub form and the
whole-file form are one bug at different points in its lifecycle.

Measurement method (may be useful to others here)

Counting missing transcripts by session-env/ directories badly over-counts, because every
launch creates one — including --resume pickers and instant exits that never write a
transcript and never should.

A cleaner discriminator: a session that actually ran turns leaves a per-session file under
~/.claude/hooks/turn-state/<uuid>.last (this is from a local PostToolUse hook, so it is
not available by default, but any per-session hook artifact works the same way). Filter to
sessions that have one, then check whether a transcript exists.

Applied to my machine over an 8-day window:

| | count |
|---|---|
| Sessions with transcripts | 103 |
| Started but never ran a turn (no transcript expected) | 25 |
| Ran real turns, transcript entirely absent | 13 |

Two data points that constrain the cause

1. Six of the 13 losses are from days with no crash, no reboot, and no unclean shutdown.
Consistent with the clean-exit reproduction reported in #78821. Whatever this is, process
termination is not required to trigger it.

2. Retention is not the cause here. cleanupPeriodDays is unset in every settings source
on this machine (user, local, project, and the backup copies), so the default of 30 applies,
and the lost sessions were same-day. I also checked specifically for cleanupPeriodDays: 0,
since the CLI's own validation message says that value "previously silently disabled all
transcript writes" — it appears nowhere in my configuration. Version-update migrations do not
line up either: 2.1.216 and 2.1.217 were installed hours after and days after the respective
loss clusters.

Untested variable: concurrency in a single project directory

The one factor that correlates with every loss cluster on my machine is many sessions
running at once, several sharing the same project directory
(the same encoded-cwd folder
under ~/.claude/projects/). In the worst window, seven sessions started within two hours in
overlapping project directories and all seven lost transcripts, while two more from the same
window survived as stubs.

I cannot prove causation, and I want to be clear that it is a correlation only. But none of
#79298, #78821, or #79758 appears to have varied this, and there is precedent for the log
writer misbehaving under concurrency in the same working tree (#66454 reports cross-session
message duplication among concurrent sessions in one project directory, plus duplicated
consecutive JSONL lines in the source session).

If a maintainer wants to test one variable, I would suggest that one.

Related, in case it helps triage

#79188 reports that transcript persistence is silently skipped depending on the hosting
terminal, with a controlled A/B between two multiplexers. I run one of the multiplexers named
there, which may be a confounder for my concurrency observation, or may be the same underlying
bug reached by a different path.

What would help users in the meantime

Any of these would reduce the damage even without a fix:

  • Do not rewrite an existing transcript on resume. Append, or write a new file.
  • Log deletions and stub-rewrites somewhere durable. Right now this failure is completely

silent: no warning, no debug entry, no trash.

  • A --no-resume-rewrite style escape hatch, or documented guidance not to resume sessions

you care about until this is resolved.

JW53222 · 1 month ago

Root cause found: inherited CLAUDE_CODE_CHILD_SESSION=1.

A session that finds CLAUDE_CODE_CHILD_SESSION=1 in its environment at startup treats itself as a managed child ("my parent persists my transcript") and silently skips writing message records — the .jsonl gets only metadata lines (ai-title, last-prompt, mode, queue-operation), and the session's prompts never reach history.jsonl. Every session in my original report was launched, directly or transitively, from a shell descended from another Claude session.

How the inheritance happens: every Claude session exports the marker to its own Bash-tool children (that part is by design — it's how true subagents know not to self-save). But the marker then rides along into anything those shells spawn: terminal emulators, launcher scripts, tmux panes. In my setup, fleet-launcher scripts run from inside a session spawn xfce4-terminal windows that exec new standalone claude --remote-control sessions — all of which inherit the marker and are gutted from birth. 27+ sessions affected since ~06-25.

Verification on live processes (2.1.218):

$ tr '\0' '\n' < /proc/<claude-pid>/environ | grep CHILD_SESSION
CLAUDE_CODE_CHILD_SESSION=1        # affected: transcript file is a stub or missing entirely

Affected sessions also inherit the parent's stale CLAUDE_CODE_SESSION_ID / CLAUDE_CODE_BRIDGE_SESSION_ID.

Triage gotcha: you cannot check this from inside a session by running env in its Bash tool — that always shows the marker, because the session sets it for its child shells. Only the claude process's exec-time environment (/proc/<pid>/environ) is meaningful. This is also why my earlier repro attempts were all negative: trivial sessions launched from clean terminals never inherit it.

Minimal repro:

  1. CLAUDE_CODE_CHILD_SESSION=1 claude in a fresh terminal (or, equivalently, launch claude from any shell spawned by another session's Bash tool).
  2. Converse a few turns; exit.
  3. The session's ~/.claude/projects/<proj>/<id>.jsonl contains no "type":"user"/"type":"assistant" records — /resume opens an empty conversation.

This also explains the full signature from the original report: subagents/*.jsonl surviving intact (the affected session is itself the parent of those and saves them normally — only its own top-level transcript is suppressed), and the silent data loss on versions ≤2.1.215. The new warning in 2.1.218 ("⚠ Transcript saving is off — inherited CLAUDE_CODE_CHILD_SESSION marker") is what finally surfaced it — thank you for that.

Suggested fix: an interactive TTY session — or anything launched with an explicit --session-id / --remote-control — is never a managed child and shouldn't honor an inherited marker. Alternatively, make the marker validatable (e.g. carry the parent PID and ignore it when the actual parent doesn't match), so stale inheritance through terminals/launchers is discarded. Our local mitigation is env -u CLAUDE_CODE_CHILD_SESSION -u CLAUDE_CODE_SESSION_ID -u CLAUDE_CODE_BRIDGE_SESSION_ID at every launcher spawn point.

shleder · 7 days ago

For the metadata-only transcript stubs, please compare the scan/diagnosis result with the surviving files and verify that snapshot leaves the Claude root unchanged.

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.

shleder · 3 days ago

For sessions where the primary transcript is truncated or overwritten to a metadata stub while surviving subagent logs remain on disk:

You can scan and inspect surviving subagent transcripts and isolate clean session data using Vetto's Claude adapter:

# 1. Discover all accessible Claude Code session roots & subagent transcripts
vetto rescue --adapter claude --root ~/.claude --json scan

# 2. Diagnose surviving session health and detect truncated records
vetto rescue --adapter claude diagnose ~/.claude/projects/.../session.jsonl

# 3. Export an isolated recovery snapshot before re-running the agent
vetto rescue --adapter claude snapshot ~/.claude/projects/.../session.jsonl --output ./recovery/session.jsonl

The adapter scans explicitly bounded roots without touching credentials (~/.claude/.credentials.json) or modifying existing session state.