v2.1.173 regression: TUI writes no transcript records when CLAUDE_CODE_CHILD_SESSION is inherited from a parent claude session — silent data loss, --resume broken
Summary
Since v2.1.173, interactive TUI sessions spawned under ConPTY (i.e., inside embedded terminals — Tauri/Electron apps using portable-pty, terminal multiplexers, IDE-like tools) never write conversation records to the session transcript. The ~/.claude/projects/<cwd-slug>/<session-id>.jsonl file contains only a single {"type":"ai-title",...} record (~100 bytes) — zero user/assistant records — neither incrementally during the session nor after a clean /exit. Consequently claude --resume <session-id> reports No conversation found with session ID: <id> and the conversation history is silently and permanently lost.
This is a regression from v2.1.169: the identical spawn path produced full transcripts on v2.1.169 earlier the same evening, and broke at the exact moment the machine auto-updated to v2.1.173.
Environment
- Windows 11 Pro (10.0.22631)
- Claude Code v2.1.173 (auto-updated from v2.1.169 on 2026-06-11 ~19:10 local; regression boundary matches exactly)
- Spawn: ConPTY via portable-pty 0.8.x (Rust) —
openpty+spawn_command, interactive TUI,claude --session-id <uuid>in a trusted project directory
Reproduction
- Spawn
claude --session-id <fresh-uuid>as an interactive TUI under ConPTY (any embedded-terminal host). - Submit a prompt (typed or bracketed paste + CR). The turn completes normally — response renders, tools run, the
Stophook fires, an AI title is generated. - Inspect
~/.claude/projects/<cwd-slug>/<uuid>.jsonl→ only theai-titlerecord. Poll as long as you like; no message records appear. - Type
/exit, wait, inspect again → unchanged. claude --resume <uuid>(same cwd) →No conversation found with session ID: <uuid>.
Evidence matrix (all same machine, same day)
| Scenario | Version | Transcript |
|---|---|---|
| TUI, plain terminal | 2.1.169 (pre-update) | ✅ full |
| TUI, plain terminal (long-running session) | 2.1.173 | ✅ full, incremental |
| Headless claude -p with --session-id | 2.1.173 | ✅ full |
| Headless -p, custom env vars + UserPromptSubmit/Stop hooks active | 2.1.173 | ✅ full |
| TUI under ConPTY (portable-pty) | 2.1.173 | ❌ ai-title stub only |
| TUI under ConPTY, after clean /exit | 2.1.173 | ❌ still stub |
Notes:
- The turn demonstrably completes in the broken cases: the AI title is generated from the prompt content, registered hooks fire (
UserPromptSubmit,Stop), tool calls execute and their side effects land. - Per-session side artifacts ARE written in the broken cases:
file-history/<session-id>/,projects/<slug>/<session-id>/tool-results/…,security_warnings_state_<session-id>.json, and the globalhistory.jsonlall update. Only the conversation records in<session-id>.jsonlare missing. - Hook presence is ruled out as the trigger (broken with hooks inert; intact in headless with hooks active).
Impact
Any tool embedding Claude Code in a terminal pane loses all conversation history silently: --resume is impossible, and users discover the loss only after closing a session. This is data loss with no error surfaced anywhere.
Expected
TUI sessions under ConPTY write transcript records as they did in v2.1.169 (and as headless mode and plain-terminal TUIs still do in v2.1.173).
Showing cached comments. Read the full discussion on GitHub ↗
7 Comments
Root cause correction — not ConPTY. The trigger is inherited
CLAUDE_CODE_CHILD_SESSION.After controlled isolation (same host, same argv, one variable at a time), the embedded-terminal framing of my original report is wrong — apologies for the misdirection. The actual trigger:
An interactive TUI session that inherits
CLAUDE_CODE_CHILD_SESSIONin its environment writes no conversation records to its transcript — only theai-titlestub. A running claude session exports this marker to all of its child processes, so any program launched from inside a claude session (dev servers, apps under test, scripts) passes it transitively to every claude TUI it spawns. Those innocent sessions silently lose their entire conversation and--resumereports "No conversation found".Minimal repro (no embedding, no ConPTY tooling needed)
set CLAUDE_CODE_CHILD_SESSION=1(any terminal;exporton POSIX)claude→ send one prompt →/exit~/.claude/projects/<slug>/<id>.jsonl→ai-titlestub only.Isolation matrix (v2.1.173, Windows 11, identical portable-pty ConPTY host, identical argv
claude --session-id <uuid> "<prompt>")| Inherited env | Transcript |
|---|---|
| none of the claude-exported vars | ✅ full (83 KB) |
| full var set from a parent claude session | ❌ stub |
|
CLAUDECODE=1+CLAUDE_CODE_ENTRYPOINT=clionly | ✅ full ||
CLAUDE_CODE_CHILD_SESSION=1+CLAUDE_CODE_SSE_PORT+CLAUDE_CODE_SESSION_ID| ❌ stub ||
CLAUDE_CODE_CHILD_SESSION=1alone | ❌ stub |ConPTY,
--session-id, opening-message argv, and hooks are all exonerated — my original matrix was confounded because the probe harness itself ran as a child of a claude session, so every "TUI under ConPTY" cell was contaminated with the marker. The turn demonstrably completes in the broken cases (Stop hook fires, ai-title generated, tool side-effects land) — only conversation-record persistence is skipped.Presumably the TUI treats the inherited marker as "I am a daemon-managed child session" (the v2.1.172 background-sessions work) and skips or redirects its own persistence. When the marker is merely inherited by an unrelated interactive session, that decision destroys user data. Headless
-pis unaffected.Workaround for anyone spawning claude from tooling
Strip
CLAUDE_CODE_CHILD_SESSION(we strip the wholeCLAUDE_CODE_*/CLAUDECODEfamily) from the child environment before spawning. Verified end-to-end: with the strip in place, panes spawned from a fully contaminated parent write full transcripts again.Suggested fix
The TUI should not treat an inherited
CLAUDE_CODE_CHILD_SESSIONas authoritative for persistence decisions — child-session identity should come from an explicit channel (flag/IPC handshake), or at minimum interactive sessions should always persist their transcripts.Confirming this is still present on v2.1.187 (the repro below ran on a machine
that upgraded 2.1.185 → 2.1.187 mid-incident), and that it triggers via a plain
GNOME Terminal, not only tmux/multiplexers (#70219).
How it got into the env (no multiplexer involved):
A Bash tool call inside a running Claude session launched a
gnome-terminal. Thegnome-terminal-serverprocess itself stays clean, but the new window's shellinherited the spawning tool env:
CLAUDE_CODE_CHILD_SESSION=1
CLAUDE_CODE_SESSION_ID=<the PARENT session's id>
CLAUDE_CODE_EXECPATH=/…/versions/2.1.185
AI_AGENT=claude-code_2-1-185_agent
Every
claudestarted in that terminal (and every tab opened from it) thenlaunched as a "child session" and wrote no
~/.claude/projects/<slug>/<id>.jsonltranscript, only the
tool-results/andsubagents/sidecars under the sessiondir. So those sessions are unrecorded and non-resumable, with no warning.
Why it was hard to spot:
claudelaunches and a versionupgrade. The inherited
CLAUDE_CODE_EXECPATH/AI_AGENTstayed pinned to theold version (2.1.185) even after the binary updated to 2.1.187, which is a
useful fingerprint.
claudeprocesses and 29 interactiveshells, all carrying the same inherited parent
CLAUDE_CODE_SESSION_ID.meta, memory files), so disk/inode/inotify checks all come back healthy and
send you looking in the wrong place.
One-liner to detect a poisoned terminal before launching claude:
echo "${CLAUDE_CODE_CHILD_SESSION:-clean}"
# or, for a running instance:
tr '\0' '\n' < /proc/$(pgrep -nx claude)/environ | grep -E 'CLAUDE_CODE_CHILD_SESSION|CLAUDE_CODE_SESSION_ID'
Workaround that fixed it (scrub the inherited markers in the interactive shell rc):
if [[ -n $CLAUDE_CODE_CHILD_SESSION ]]; then
unset CLAUDE_CODE_CHILD_SESSION CLAUDE_CODE_SESSION_ID CLAUDE_CODE_EXECPATH AI_AGENT
fi
Safe because Claude's own tool subprocesses run non-interactively (
bash -con asnapshot) and never source the interactive rc, so legitimate child-session marking
is untouched; only terminals a human opens get scrubbed.
Echoing the fix proposed in #68534: a top-level interactive TUI should validate an
inherited Claude session env at startup and either ignore it (start a fresh
top-level session) or warn loudly, rather than silently suppressing transcript
creation. The silent zero-transcript path is the data-loss bug; the non-resumable
marking on genuine nested sessions is fine.
Independent confirmation on Linux (Ubuntu 24.x, Claude Code 2.1.200) — this is not ConPTY/Windows-specific. Any interactive
claudelaunched from a terminal emulator that is a descendant of a running Claude session's Bash tool inherits the parent's env and silently skips persistence.Scale datapoint: we run automation that spawns one interactive session per git worktree (
alacritty -e bash launcher.sh→claude --session-id <fresh-uuid> "<prompt>", detached withsetsid). Every such session between 2026-06-12 and 2026-07-03 left zero transcripts. We only discovered it after a machine crash, when none of the ~8 in-flight sessions could be--resumed — weeks of sessions were unrecoverable. The failure is deceptively partial: hooks fire, OTLP telemetry flows, and the sidecar dirs (~/.claude/projects/<slug>/<sid>/subagents/,workflows/) are written — only the main<sid>.jsonland the~/.claude.jsonproject entry are missing, so everything looks alive while nothing is recoverable.Root cause confirmed from a live broken session (
/proc/<pid>/environ):Controlled A/B on 2.1.200 (same trusted dir, same command, PTY via
script(1)):env -u CLAUDECODE -u CLAUDE_CODE_CHILD_SESSION -u CLAUDE_CODE_SESSION_ID claude --session-id <uuid> "..."→ transcript appears within seconds(
claude -pprint mode persists in both cases — only interactive sessions are affected, which is why CI/headless harnesses never notice.)Two observations from the shipped binary:
tmux show-environment -g CLAUDE_CODE_CHILD_SESSION) — i.e. the false-positive-inheritance problem is known and worked around for one spawn path. A fresh interactive TTY session is the same false positive via every other spawn path (terminal emulators, ConPTY,setsid).CLAUDE_CODE_FORCE_SESSION_PERSISTENCEthat also works as a workaround.Suggested fix: an interactive session attached to a fresh TTY that the harness did not itself spawn should not silently drop persistence based on an inherited env var — either ignore
CLAUDE_CODE_CHILD_SESSIONwhen stdin is a TTY and the session id doesn't match a live parent, or at minimum print a loud warning that the session will not be resumable.Workaround for anyone else spawning sessions programmatically: unset
CLAUDECODE CLAUDE_CODE_CHILD_SESSION CLAUDE_CODE_SESSION_ID CLAUDE_CODE_ENTRYPOINT CLAUDE_CODE_EXECPATHin your launcher before exec'ingclaude(or exportCLAUDE_CODE_FORCE_SESSION_PERSISTENCE=1).Related: #73294, #73848, #68534, #72347 (closed as dup).
macOS confirmation (Darwin 25.5.0, Claude Code 2.1.200), consolidating from #73848 - same trigger, isolated to the single var: interactive PTY sessions with the full inherited env never write the transcript; identical launch minus only
CLAUDE_CODE_CHILD_SESSIONwrites within seconds (A/B/C table in #73848).One macOS-specific leak vector worth noting for the fix:
open -apropagates the caller's environment to the launched app, so a GUI app relaunched from inside a Claude session poisons every descendant it ever spawns - in our case a terminal-multiplexer app whose embedded panes each ranclaude; an app restart destroyed several hours-long conversations at once. Sidecars (tool-results/,session-env/) are still written to the real project dir, which makes the loss look like deletion rather than never-written.Timing datapoint consistent with the v2.1.173 window: the poisoned app environment only arose the same day we auto-updated to 2.1.200, so we cannot narrow the regression version further, but 2.1.200 is definitely affected on macOS.
Still present in 2.1.201 (macOS, released ~Jul 4): same controlled A/B as before - interactive PTY session with
CLAUDE_CODE_CHILD_SESSION=1inherited never writes its transcript after a completed turn; identical launch with the var stripped writes ~27 KB within seconds.Still here in 2.1.205 (macOS native), and I hit it through a propagation path worth flagging.
I run cmux, a Ghostty-based terminal multiplexer. It got launched from inside a Claude session so the cmux process inherited
CLAUDE_CODE_CHILD_SESSION=1andCLAUDE_CODE_SESSION_ID=<parent>— and it's been handing that env to every pane it spawns ever since. Each pane is a genuine top-level session (noclaudeancestor in the process tree, launched with its own--session-id), but the inherited marker makes it skip the transcript. Nine new sessions in a row, zero.jsonlfiles, no warning. I caught it because sessions couldn’t be resumed.Still present in 2.1.207 (macOS, Apple Silicon). Same root cause everyone else has landed on: inherited
CLAUDE_CODE_CHILD_SESSION.My propagation path is the same shape as colinc's cmux report, just a different multiplexer. I run herdr, which spawns panes via portable-pty. The herdr server is a long lived daemon, and I happened to start it from inside a Claude Code Bash call. So the server picked up
CLAUDE_CODE_CHILD_SESSION=1(plus the parent'sCLAUDE_CODE_SESSION_ID) and then handed that env to every pane it ever spawned. Every interactive Claude in every pane silently stopped writing transcripts.ps eww <server-pid>shows the parent session's ID stamped right on the daemon, which is a quick way to confirm it if anyone else is chasing this.Worth stressing for anyone triaging: the multiplexer is irrelevant. It's just a long lived process that outlives the Claude session it was born in. A dev server, a daemon, a terminal emulator, anything. Once it has the var, everything downstream of it is poisoned, potentially for days, and you don't find out until you go looking for a session that isn't there.
A working override, which I haven't seen mentioned in this thread or anywhere in the repo
Set that on the session (or on whatever process spawns it) and transcripts come back immediately, even with a poisoned parent env.
Verified on 2.1.207 against an already poisoned herdr server. Same server, same panes, two agents launched back to back, one with the var and one without. The one without produced
tasks/<id>/,session-env/<id>/, andprojects/<cwd>/<id>/but no.jsonl. The one with it wrote the transcript normally and shows up in--resume. So if you're bleeding sessions right now, this unblocks you today without waiting on a fix.It does not make subagents start writing sessions, which was my first worry. The gate is
isInteractive-scoped, so it never applied to subagents to begin with (they persist intoprojects/<cwd>/<id>/subagents/either way). Setting the override flips a branch that was already returning the same answer for them.Why some people hit this and others don't
The skip logic has an escape hatch, but it only knows about tmux. From the shipped 2.1.207 bundle:
Note that the hatch requires
$TMUXand the var to be present in tmux's global environment. That explains a few things in this thread. It's why hampsterx still got bitten in a plain GNOME Terminal, and it's why every non-tmux host (herdr, cmux, Ghostty based terminals, editor embedded terminals) is completely exposed. tmux got a special case; nothing else did.Two asks, in the order I'd care about them
1. Say something. The suppression is silent, and it's worse than silent: the Stop hook still emits a
transcript_pathpointing at a.jsonlthe code has already decided it will never write.tasks/,session-env/, andprojects/<id>/all get created too. Every observable signal says "session saved fine," and then--resumejust quietly doesn't list it. A single warning line at startup, or a note in--resumewhen a session directory exists with no transcript, would have saved everyone in this thread a lot of time. Right now the failure mode is indistinguishable from success until the data is already gone.2. Stop keying off an inheritable env var. Claude already tracks
agentId/teamNamefor real agent team members, andog()above literally checks for them. An env var will leak into any long lived child forever, which is precisely the failure every person in this thread hit. At minimum, don't let it survive into a process that isn't a direct child.Happy to add more detail on the herdr repro if useful, but the minimal repro is already here and needs no multiplexer at all: export the var, run interactive
claudein a pty, do one turn, exit, and there's no transcript.