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

Status Fixed / completed
Reported on v2.1.169
Maintainer reply None cached
Activity 8 comments · opened Jun 11, 2026 · closed Aug 17, 2026

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

  1. Spawn claude --session-id <fresh-uuid> as an interactive TUI under ConPTY (any embedded-terminal host).
  2. Submit a prompt (typed or bracketed paste + CR). The turn completes normally — response renders, tools run, the Stop hook fires, an AI title is generated.
  3. Inspect ~/.claude/projects/<cwd-slug>/<uuid>.jsonl → only the ai-title record. Poll as long as you like; no message records appear.
  4. Type /exit, wait, inspect again → unchanged.
  5. 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 global history.jsonl all update. Only the conversation records in <session-id>.jsonl are 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).

View original on GitHub ↗

7 Comments

Nejjad · 2 months ago

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_SESSION in its environment writes no conversation records to its transcript — only the ai-title stub. 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 --resume reports "No conversation found".

Minimal repro (no embedding, no ConPTY tooling needed)

  1. set CLAUDE_CODE_CHILD_SESSION=1 (any terminal; export on POSIX)
  2. claude → send one prompt → /exit
  3. ~/.claude/projects/<slug>/<id>.jsonlai-title stub only.
  4. Unset the var, repeat → full transcript.

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=cli only | ✅ full |
| CLAUDE_CODE_CHILD_SESSION=1 + CLAUDE_CODE_SSE_PORT + CLAUDE_CODE_SESSION_ID | ❌ stub |
| CLAUDE_CODE_CHILD_SESSION=1 alone | ❌ 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 -p is unaffected.

Workaround for anyone spawning claude from tooling

Strip CLAUDE_CODE_CHILD_SESSION (we strip the whole CLAUDE_CODE_*/CLAUDECODE family) 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_SESSION as 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.

hampsterx · 2 months ago

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. The
gnome-terminal-server process itself stays clean, but the new window's shell
inherited 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 claude started in that terminal (and every tab opened from it) then
launched as a "child session" and wrote no ~/.claude/projects/<slug>/<id>.jsonl
transcript
, only the tool-results/ and subagents/ sidecars under the session
dir. So those sessions are unrecorded and non-resumable, with no warning.

Why it was hard to spot:

  • It persisted ~36h across dozens of independent claude launches and a version

upgrade. The inherited CLAUDE_CODE_EXECPATH/AI_AGENT stayed pinned to the
old version (2.1.185) even after the binary updated to 2.1.187, which is a
useful fingerprint.

  • Blast radius on one machine: 13 live claude processes and 29 interactive

shells, all carrying the same inherited parent CLAUDE_CODE_SESSION_ID.

  • Everything except the root transcript writes fine (tool-results, subagent

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 -c on a
snapshot) 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.

DuaneNielsen · 1 month ago

Independent confirmation on Linux (Ubuntu 24.x, Claude Code 2.1.200) — this is not ConPTY/Windows-specific. Any interactive claude launched 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.shclaude --session-id <fresh-uuid> "<prompt>", detached with setsid). 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>.jsonl and the ~/.claude.json project entry are missing, so everything looks alive while nothing is recoverable.

Root cause confirmed from a live broken session (/proc/<pid>/environ):

CLAUDECODE=1
CLAUDE_CODE_CHILD_SESSION=1
CLAUDE_CODE_SESSION_ID=<the PARENT session's uuid>

Controlled A/B on 2.1.200 (same trusted dir, same command, PTY via script(1)):

  • inherited vars → transcript never created
  • env -u CLAUDECODE -u CLAUDE_CODE_CHILD_SESSION -u CLAUDE_CODE_SESSION_ID claude --session-id <uuid> "..." → transcript appears within seconds

(claude -p print mode persists in both cases — only interactive sessions are affected, which is why CI/headless harnesses never notice.)

Two observations from the shipped binary:

  1. There is already a tmux special-case (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).
  2. There is an undocumented escape hatch CLAUDE_CODE_FORCE_SESSION_PERSISTENCE that 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_SESSION when 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_EXECPATH in your launcher before exec'ing claude (or export CLAUDE_CODE_FORCE_SESSION_PERSISTENCE=1).

Related: #73294, #73848, #68534, #72347 (closed as dup).

JackPayne123 · 1 month ago

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_SESSION writes within seconds (A/B/C table in #73848).

One macOS-specific leak vector worth noting for the fix: open -a propagates 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 ran claude; 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.

JackPayne123 · 1 month ago

Still present in 2.1.201 (macOS, released ~Jul 4): same controlled A/B as before - interactive PTY session with CLAUDE_CODE_CHILD_SESSION=1 inherited never writes its transcript after a completed turn; identical launch with the var stripped writes ~27 KB within seconds.

colinc · 1 month ago

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=1 and CLAUDE_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 (no claude ancestor 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 .jsonl files, no warning. I caught it because sessions couldn’t be resumed.

mhaslinsky · 1 month ago

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's CLAUDE_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

CLAUDE_CODE_FORCE_SESSION_PERSISTENCE=1

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>/, and projects/<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 into projects/<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:

function U_t(){                                                   // shouldSkipPersistence
  if (be.CLAUDE_CODE_FORCE_SESSION_PERSISTENCE) return false;
  if (!(be.CLAUDE_CODE_CHILD_SESSION && c1() && !og())) return false;
  return !ZYm();
}
function c1(){ return Pt.isInteractive }
function og(){ if (EN()) return true; return !!(Rie?.agentId && Rie?.teamName) }  // real agent-team member
function eJm(){
  if (!be.TMUX) return false;
  spawnSync("tmux", ["show-environment","-g","CLAUDE_CODE_CHILD_SESSION"], ...)   // tmux, and only tmux
}

Note that the hatch requires $TMUX and 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_path pointing at a .jsonl the code has already decided it will never write. tasks/, session-env/, and projects/<id>/ all get created too. Every observable signal says "session saved fine," and then --resume just quietly doesn't list it. A single warning line at startup, or a note in --resume when 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 / teamName for real agent team members, and og() 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 claude in a pty, do one turn, exit, and there's no transcript.

Showing cached comments. Read the full discussion on GitHub ↗