🧹 Stop a hosted Claude session inheriting its launcher session marker - #162
Merged
Conversation
A `claude` backend run launched from inside a Claude Code session hung until
its timeout, having completed the turn on screen. The child inherited
`CLAUDE_CODE_CHILD_SESSION`, so Claude Code treated it as a nested session and
disabled transcript persistence:
⚠ Transcript saving is off — inherited CLAUDE_CODE_CHILD_SESSION marker
The transcript is the only end-of-turn signal that backend has. `ScanTranscriptTick`
returns early forever on a file that is never written, `end_turn` never arrives,
and the run reports nothing at all — not even a failure. This is the
configuration the avenor MCP plugin runs in, and PTY is the default terminal.
The process a launcher starts is an independent session with its own
`--session-id`, so it no longer inherits the launching session's identity:
`CLAUDE_CODE_CHILD_SESSION`, `CLAUDE_CODE_SESSION_ID`, and `CLAUDE_PID`.
Each launcher needs a different mechanism. `PTYLauncher` owns the child
environment directly, so it filters `os.Environ` through `ScrubParentClaudeEnv`.
A tmux pane inherits the tmux *server's* environment, which avenor does not own
and cannot filter — a server started earlier from inside a Claude Code session
hands the marker to every pane for the rest of its life. `tmuxNewSessionArgs`
therefore strips the variables inside the command, where the server's
environment no longer matters.
The prefix uses the `unset` builtin rather than `env -u` because the commands it
wraps begin with `exec`, which is also a builtin: `env -u X exec claude` sends
env looking for a binary named "exec". `unset` also adds no process, so
`#{pane_pid}` still reports claude's own PID.
Verified end to end with the marker set in the parent, against Claude Code
v2.1.222: a healthy `--agent mule` run went from a 150s+ silent hang to
`session.end` `done` / `end_turn` in 5s under PTY and 4s under tmux.
AI-Generated-By: Claude (Opus 5)
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
- Drive the tmux scrub assertion through TmuxLauncher.Start via a fake tmux binary injected into PATH (covers the actual new-session wiring) - Defer captureUntil's deadline to the polling context so the timeout is not a flakiness vector under load - Add empty-input boundary case for ScrubParentClaudeEnv - Scrub PTY env after the final append so a caller-provided Env entry cannot reintroduce a scrubbed session identity; regression test covers all three identity variables via both inherited and Env-sourced paths - Extend live-shell composition and PTY tests to assert all three scrub targets, not just CLAUDE_CODE_CHILD_SESSION
Owner
Author
|
Addressed 3 review comments across 3 files (commit
Review loop: 2 iterations. Iteration 2 was run against the working tree after fixes:
|
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What
A
claudebackend run launched from inside a Claude Code session hung until its timeout — after completing the turn on screen. The child inheritedCLAUDE_CODE_CHILD_SESSION, so Claude Code treated it as a nested session and turned off transcript persistence, saying so in a startup banner:The transcript is the only end-of-turn signal that backend has.
ScanTranscriptTickreturns early forever on a file that is never written,end_turnnever arrives, and the run reports nothing at all — not a failure, not a timeout error, nothing afteragent.prompt_submitted.This is the configuration the avenor MCP plugin runs in, and PTY is the default terminal.
The process a launcher starts is an independent session with its own
--session-id, so it no longer inherits the launching session's identity:CLAUDE_CODE_CHILD_SESSION,CLAUDE_CODE_SESSION_ID, andCLAUDE_PID. Only the first is known to change behaviour; the other two are dropped so a hosted session cannot be mistaken for its launcher.Two mechanisms, because tmux is not ours
PTYLauncheros.EnvironviaScrubParentClaudeEnvTmuxLauncherUnsetParentClaudeEnvPrefixThe tmux case is the one worth reading twice. avenor does not own the tmux server and cannot filter its environment. A server started earlier from inside a Claude Code session hands the marker to every pane it creates for the rest of its life, and
tmux new-session -ecan only assign a variable, not remove one. Stripping inside the command works whichever environment the server holds — including that already-poisoned server.The prefix uses the
unsetbuiltin rather thanenv -u, because the commands it wraps begin withexec, which is also a builtin.env -u X exec claudesends env looking for a binary namedexecand fails.unsetalso adds no process, so#{pane_pid}still reports claude's own PID.TestUnsetParentClaudeEnvPrefixRunsUnderShellWithExecpins that down — it fails against anenv -uimplementation.Verification
Test-first: every test here fails before the fix.
go build ./...,go vet ./internal/runtime/..., and the fullgo test ./...(38 packages) pass, as doesgo test -race -count=1 ./internal/runtime/claudecore/....End to end with
CLAUDE_CODE_CHILD_SESSIONset in the parent, against Claude Code v2.1.222, a healthy--agent mulerun:session.enddone/end_turnin 5sdone/end_turnin 4stmux passing beforehand was an accident of my own tmux server predating the Claude Code session that launched avenor. On a machine where avenor starts the server, tmux fails the same way and then stays broken for that server's lifetime.
The tmux test asserts on the rendered argv rather than launching a pane.
t.Setenvcannot reach an already-running tmux server, so an end-to-end assertion there would pass whether or not the scrub exists.Related
Independent of #161 and mergeable in either order — that PR makes a refused launch report
failedinstead ofdone; this one fixes a launch that succeeds and is then never reported at all. #161's body lists this bug under adjacent work.Still open, found alongside these two and not addressed by either:
TranscriptPathdoes not resolve symlinks, so-dir /tmpreadsprojects/-tmp/while Claude Code writesprojects/-private-tmp/— the same class of silent hang, from a different cause.🤖 Generated with Claude Code