release/0.2.3: spawned agents persist transcripts (parent-agent env scrub)#12
Merged
Conversation
…s transcript When claudemux runs from inside another Claude Code session, the child `claude` inherited CLAUDECODE / CLAUDE_CODE_* / AI_AGENT and tripped claude's nested-session detection, suppressing transcript persistence (only an async ai-title record, no user/assistant turns). claudemux drives agents by reading that transcript, so the conversation read back empty — the recurring "0 messages / gate stalls / delivery-unconfirmed" gremlin. Root-caused via A/B across claude 2.1.168 and 2.1.169 (identical — NOT a version regression): the fix is a true unset at the pane-launch boundary, `env -u <vars> -- claude <argv>`. `-e VAR=` only blanks (still trips the heuristic) and the shared persistent tmux server bakes its first process's env (so process.env mutation no-ops) — the unset must happen at exec. A neutral `unsetEnv: string[]` is threaded buildArgv -> spawn-boot -> backend.spawn -> newSession; the claude-specific var list (NESTED_AGENT_ENV) lives only in src/agents/claude.ts (layering grep T12). The existing `env` passthrough stays the escape hatch (a deliberate re-set wins; `-u` is dropped on overlap). cmd/argv are byte-identical — the --session-id two-element security invariant is untouched. Defense-in-depth (ADR 0008 follow-up): validate `unsetEnv` names at the backend boundary (InvalidEnvVarName, POSIX identifier allow-list). Not exploitable today (trusted sole producer, shell-free execFile spawn) but closes the seam against a future untrusted producer. See docs/decisions/0008-scrub-parent-agent-env-for-transcript-persistence.md. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
CI `gates` runs `biome check .` (lint + format); the prior commit was verified with `biome lint` only, so formatter-canonical layout was missed: - collapse the `it.each([...])` / `toEqual([...])` arrays in validate.test.ts and sessions.test.ts to biome's single-line form; - restore package.json's single-line `keywords`/`files` arrays (the version bump's JSON.stringify had expanded them, which biome rejects). No semantic change. Full `gates` job (check, typecheck, build, safety-grep, layering-grep, no-tmux-in-public, assert-npm-pack) passes locally. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Adds test/session/transcript-persistence.live.test.ts — a maintainer acceptance test (gated CLAUDEMUX_LIVE_TRANSCRIPT_PERSISTENCE=1, excluded from CI in vitest.config.ts, Linux-only via /proc) that proves the parent-agent env scrub works end-to-end against a real claude on a nested-booted shared tmux server. The test asserts the fix's OBSERVABLE EFFECT, not claude's suppression behaviour. An expanded A/B (claude 2.1.168/2.1.169/2.1.170) found the nested-session suppression is environment-fragile: the same binary suppresses or persists depending on which other env vars accompany CLAUDECODE (a node/npm launch context flips it). A behavioural "did claude suppress?" assertion is therefore a non-deterministic guard — a first draft asserted persistence and gave a false pass (its negative control did not fail). Instead the test boots a tmux server with all five nested vars in its global env, spawns claude via the backend, and asserts all five are scrubbed from the live pane's /proc/<pid>/environ. Negative control verified: passes with the fix, fails (all five leak) when the `env -u` emission is reverted. ADR 0008 Evidence updated with this refinement; the scrub decision is unchanged — only the form of the proof moves from "observe claude persist" to "observe claudemux scrub." Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
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.
Release 0.2.3
The bug
When claudemux is run from inside another Claude Code session (dev, CI, nested agents), the spawned
claudeinherited the parent'sCLAUDECODE/CLAUDE_CODE_*/AI_AGENTenvironment variables, tripped claude's nested-session detection, and suppressed its own transcript persistence — writing only an asyncai-titlerecord, nouser/assistantturns. claudemux drives agents by reading that transcript, so the conversation read back empty: the recurring "0 messages / gate stalls / delivery-unconfirmed" gremlin.Root cause (A/B-proven, not a version regression)
Verified across claude 2.1.168 and 2.1.169 — behaviour identical, so this was never a claude version regression and never a flush-timing race. The discriminator is purely the inherited env. Unsetting any single var doesn't help; the whole set must be unset, and it must be a true unset at exec time:
-e VAR=only blanks the var (still present → heuristic still trips).process.envbefore spawn no-ops when the server was first booted from a nested shell.The fix
Launch the pane under
env -u <vars> -- claude <argv>. A neutralunsetEnv: string[]is threadedbuildArgv → spawn-boot → backend.spawn → newSession; the claude-specific var list (NESTED_AGENT_ENV) lives only insrc/agents/claude.ts(layering-grep T12).cmd/argvare byte-identical — the--session-idtwo-element security invariant is untouched. The existingenvpassthrough stays the escape hatch (a deliberate re-set wins;-uis dropped on overlap).Defense-in-depth (ADR 0008 follow-up)
unsetEnvnames are validated at the backend boundary (InvalidEnvVarName, POSIX-identifier allow-list). Not consumer-exploitable today (trusted sole producer, shell-freeexecFilespawn) — closes the seam against any future untrusted producer.Design & review trail
0008-scrub-parent-agent-env-for-transcript-persistence.md(+ Follow-up note for the validation).Verification
typecheck,lint(biome, 118 files),scripts/layering-grep.sh— all green.CLAUDECODE=1, then spawn from a clean shell, assert realuser/assistantrecords) is included as a documentedit.skip— it needs live-harness scaffolding to set the server-boot env and assert record types (the current live harness asserts only file existence, which the suppressedai-title-only case would falsely pass). Tracked as follow-up.🤖 Generated with Claude Code