[BUG] Transcript persistence is silently skipped depending on the hosting terminal — controlled A/B (multiplexer pane vs tmux) isolates the terminal as the only variable

Status Fixed / completed
Reported on v2.1.162
Maintainer reply None cached
Activity 4 comments · opened Jul 19, 2026 · closed Jul 30, 2026

What happened

When Claude Code runs inside a pane of the herdr terminal multiplexer, the session transcript .jsonl is never written — not during the session, and not on a clean /exit. The session is silently and permanently unrecoverable: --resume cannot find it, and no error or warning is shown at any point.

The same Claude Code binary, same version, same user, same machine, same environment variables and same cwd persists transcripts normally when launched from tmux instead.

Failure signature (constant across every failing run)

~/.claude/session-env/<sid>/                       ✅ created
~/.claude/projects/<slug>/<sid>/tool-results/      ✅ created and populated
~/.claude/projects/<slug>/<sid>.jsonl              ❌ never created

Everything written eagerly during the session survives; only the transcript — the part that appears to be buffered and flushed later — is missing. ~/.claude/history.jsonl also does not receive the session's prompts.

Controlled experiment

Four runs, driven programmatically (herdr socket API / tmux send-keys) so the interaction is identical. Each: new session, 2–3 turns, clean /exit.

| # | Terminal | cwd | HERDR_* + CLAUDE_CODE_* env | transcript |
|---|---|---|---|---|
| A | herdr pane | ~ | present | ❌ none |
| B | herdr pane (--debug-file) | ~ | present | ❌ none |
| C | tmux, launched from inside a herdr pane | scratch dir | present (inherited) | ✅ 83 KB |
| D | tmux, launched from inside a herdr pane | ~ | present (inherited) | ✅ written |

C and D inherit the full herdr environment: HERDR_ENV, HERDR_PANE_ID, HERDR_SOCKET_PATH, plus an inherited CLAUDECODE=1 / CLAUDE_CODE_CHILD_SESSION=1 / CLAUDE_CODE_SESSION_ID=… set (the multiplexer's server process carries these, so every pane it spawns inherits them). They persist normally.

→ The only variable that changes the outcome is the terminal/PTY itself.

The passing control was verified end to end

To be sure "passes" means more than "a file exists", the tmux control was re-run with three individually identifiable turns:

  1. transcript file written on /exit (89 KB);
  2. parsed — all three turns present, including the final one, plus the /exit record;
  3. claude --resume <id> re-launched and the full conversation rendered correctly.

The failing (multiplexer) runs produce no file at all, so there is nothing to resume.

Process and terminal state are effectively identical

Comparing the claude process in a multiplexer pane vs in tmux (ps -o pid,ppid,pgid,sess,tpgid,tty,stat, stty -a, ancestor chain): both have a controlling tty, both are in the foreground process group, neither is a session leader, ancestor chains are structurally identical (claude → shell → multiplexer server → launchd), and terminal line settings match. The only two differences are the pane subtree running at low priority (SN+ vs S+) and one input flag (-iutf8 vs iutf8) — neither can plausibly prevent a file write.

Corroborating this: during a failing run, ~/.claude.json is written atomically three times at shutdown. File I/O is fully functional at exactly the moment the transcript is not written.

Ruled out (each with a control, not by inference)

  • Environment variables — runs C and D carry the identical HERDR_* and inherited CLAUDE_CODE_* variables and write fine. A nested claude -p … spawned from a Bash tool call inside a herdr pane (same env) also writes its transcript normally.
  • cwd / project slug — run D uses the same ~ cwd as run A, opposite result.
  • Multiplexer auto-detection (cf. #70219) — the debug log explicitly shows no multiplexer detected:

DECSTBM: enabled (TMUX=unset ZELLIJ=unset TERM_PROGRAM=<redacted> TERM=xterm-256color)
and the process command line is a bare claude with no injected --teammate-mode.

  • Unclean shutdown — the debug log shows all SessionEnd hooks completing with status 0, followed by ~/.claude.json being written atomically three times. It is a fully normal shutdown; the transcript write simply never happens.
  • The multiplexer deleting files — strings on the herdr binary shows no code path touching .claude/projects or .jsonl; the upstream maintainer has confirmed the integration only writes a hook script and reports session_id over a socket. Independently: a Codex CLI session running in an adjacent pane of the same multiplexer writes its rollout .jsonl incrementally and survives intact, so the pane environment is perfectly capable of hosting a durable agent session.
  • Retention cleanup — cleanupPeriodDays is set to 3650.

Debug log

--debug-file on a failing run produces zero transcript-related entries — no append, write, flush, open, or error concerning the session .jsonl. The persistence code path appears never to be entered. (Same observation as #70219.)

What I am asking for

I am not claiming to know where the defect lives — the trigger may well be something the multiplexer does to its PTY, and I have shared these findings with that project too. What I can show is that Claude Code's transcript persistence has an undocumented, invisible dependency on the hosting terminal, and that when the dependency is not met it fails silently and totally. Three asks, in priority order:

  1. Do not fail silently. Whatever the trigger, if a session will not be persisted, surface it — at startup, on exit, or both. Today the only feedback is --resume coming up empty, potentially hours later, with ~/.claude/projects/<slug>/<sid>/ sitting there full of sidecar files that make it look like the session was recorded. Even if the root cause turns out to be external, this part is fixable here and would have prevented the loss.
  2. Document or remove the dependency. If persistence is legitimately conditional on some terminal property, that condition should be documented and ideally logged under --debug-file (today the debug log contains zero transcript-related entries, so there is no way to diagnose this from the outside). If it is not intentional, it is a bug.
  3. Consider writing incrementally rather than buffering until exit (see #70632), so that any abnormal death costs the last turn rather than the entire session.

Impact

Silent, total, unrecoverable loss of whole sessions. Nothing warns the user during the session, and ~/.claude/projects/<slug>/<sid>/ exists with sidecar files — which makes it look like the session was recorded.

This has been hit before and misdiagnosed: herdr #454 ("Claude sessions lost!", Fedora/kitty, CC 2.1.162) was closed on the correct observation that the multiplexer does not touch ~/.claude/projects — but the actual mechanism was never identified, so it remains unfixed.

Environment

  • Claude Code: 2.1.215 (latest at time of filing; native install)
  • OS: macOS
  • Terminal: herdr 0.7.4, TERM=xterm-256color
  • Control terminal: tmux (same machine, same shell, same env) — works

Related

  • #70632 — transcript not written until session exit (deferred flush). Likely the same subsystem; this report is the case where the flush never happens at all.
  • #70219 — multiplexer auto-detection disables persistence. Different trigger — that one shows an injected --teammate-mode; here no multiplexer is detected and no such flag is injected.
  • #63545 — transcript not saved under tmux. Also different: plain tmux persists correctly on this setup (runs C and D above).

View original on GitHub ↗

3 Comments

haoliangli · 2 months ago

Follow-up data point on ask #1 (don't fail silently).

Two sessions were started in a multiplexer pane today and closed normally. Both left the usual artifacts on disk:

~/.claude/session-env/<sid>/                    present
~/.claude/projects/<slug>/<sid>/tool-results/   present, populated
~/.claude/projects/<slug>/<sid>.jsonl           absent

Attempting to resume either one:

$ claude --resume <sid>
No conversation found with session ID: <sid>

That one line is the entire feedback the user ever gets. At that moment they are holding a valid session id, and the session's own directory is sitting right there with sidecar files in it — everything looks like the session was recorded. There is no hint that persistence was skipped, no pointer to why, and nothing was surfaced earlier, while it could still have been acted on.

Whatever the eventual root cause turns out to be, this is the part that turns a recoverable situation into silent data loss.

haoliangli · 2 months ago

Resolved on 2.1.220 — no longer reproduces.

Re-tested today on the same machine and the same multiplexer (herdr 0.7.4, unchanged since the original report). Claude Code is now 2.1.220; this was filed against 2.1.215.

The transcript is now written incrementally while the session runs, so the failure mode described here is gone:

~/.claude/projects/<slug>/<sid>.jsonl   — one live session in a multiplexer pane, never exited

  Jul 29 18:41      152,619 bytes
  Jul 29 23:25      926,554 bytes
  Jul 30 07:14    1,052,037 bytes

Previously this file was never created at all in a multiplexer pane, even after a clean /exit. It now appears early in the session and grows continuously — which also means an unclean exit no longer costs the entire session, only the tail.

I can't attribute the fix. I don't see a matching changelog entry, so I don't know which patch in the 2.1.215 → 2.1.220 range changed this, or whether it was aimed at this at all.

One data point that may be useful for #70632 (deferred-until-exit): the deferred-write behavior itself is also gone here — though that issue is filed against Linux and this observation is macOS.

Closing as resolved. Happy to reopen with fresh data if it returns.

SSkenter · 2 months ago

Additional data point (Windows) + a hard-loss consequence that supports ask #1 (fail loudly).

Same failure signature, different host: Windows 11 Enterprise (26200), Claude Code ~2.1.21x at session start, interactive session in Windows Terminal with teammateMode: "tmux" and CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS=1 (the session was an agent-teams lead, backendType: in-process):

~/.claude/session-env/<sid>/                      ✅ created
~/.claude/teams/session-<sid8>/config.json        ✅ created
~/.claude/projects/<slug>/<sid>/subagents/        ✅ populated (11 agents)
~/.claude/projects/<slug>/<sid>/tool-results/     ✅ populated
~/.claude/file-history/<sid>/                     ✅ populated
~/.claude/projects/<slug>/<sid>.jsonl             ❌ never created

Independent corroboration that the file never existed at any point (not deleted later): a Stop hook that reads transcript_path from the hook payload fired ~50 times across the session's life and always found the path non-existent, from the first turn onward.

Why this deserves the loud failure: the session process started Aug 9 on a pre-2.1.220 build and stayed alive as a long-lived tmux session for 4 days — so it never picked up the 2.1.220 incremental-write fix. An overnight OS-update reboot (Aug 13, 03:30) killed the process; with no on-disk transcript and no exit-time flush, 4 days of conversation are permanently unrecoverable and --resume lists nothing. A second session on the same machine (different project, started Aug 8, same signature) was lost the same way.

Re-tested today on 2.1.229 on the same machine: live sessions now write <sid>.jsonl incrementally, so the persistence fix holds here too. The remaining gap is exactly ask #1: a session that is not persisting its transcript should warn or refuse to continue, because silent skip + long-lived process + forced reboot = total loss with no recovery path.

🤖 Generated with Claude Code

Showing cached comments. Read the full discussion on GitHub ↗