Skip to content

fix: strip all Claude Code session env vars from child processes - #430

Open
alekb wants to merge 1 commit into
umputun:masterfrom
alekb:fix/strip-claude-code-session-env
Open

fix: strip all Claude Code session env vars from child processes#430
alekb wants to merge 1 commit into
umputun:masterfrom
alekb:fix/strip-claude-code-session-env

Conversation

@alekb

@alekb alekb commented Aug 12, 2026

Copy link
Copy Markdown
Contributor

Problem

Stripping CLAUDECODE alone stopped being enough once Claude Code 2.1.x began marking spawned processes with a family of per-session vars. Any one of them left in the child env makes the spawned claude behave as a nested session: it attaches to the parent's session plumbing and never writes its own transcript. A PTY wrapper such as fya then waits for output that never arrives, and every turn burns the full --turn-timeout (30m by default) having done no work.

The failure is near-silent — the only symptom is FYA_TRANSIENT_TIMEOUT, which ralphex correctly classifies as transient and retries, so the run loops at 30 minutes per iteration making no progress.

Evidence

Running ralphex from inside a Claude Code session, Claude Code 2.1.228, fya 0.4.0, ralphex v1.6.1. /proc/<pid>/environ on the fya child:

CLAUDECODE                     (correctly stripped)
CLAUDE_CODE_CHILD_SESSION      set
CLAUDE_CODE_SESSION_ID         set
CLAUDE_CODE_MESSAGING_SOCKET   set
CLAUDE_CODE_MESSAGING_TOKEN    set
CLAUDE_CODE_BRIDGE_SESSION_ID  set
CLAUDE_CODE_ENTRYPOINT         set
CLAUDE_CODE_EXECPATH           set
CLAUDE_PID                     set
CLAUDE_EFFORT                  set

Two iterations timed out at 30m each, with no session transcript written under ~/.claude/projects/<cwd-slug>/ for the entire run. Relaunching the same plan with the full set scrubbed (env -u … ralphex …) produced a child transcript in ~85 seconds and normal streaming progress.

Change

Collect the markers in sessionEnvVars and strip them in both executors. claudeChildEnv (executor.go) and the codex runner's childEnv (codex.go) have the same defect, and in a full-mode run the codex loop is reached by the same invocation, so both are fixed here rather than split.

Deliberately an explicit list rather than a CLAUDE_CODE_* prefix strip: user-set config vars share that prefix (CLAUDE_CODE_USE_BEDROCK, _USE_VERTEX, _MAX_OUTPUT_TOKENS, _SUBAGENT_MODEL) and must still reach the child. A test covers that so the distinction doesn't get "simplified" away later.

Tests

Cases added to the existing tables in TestClaudeChildEnv and TestExecCodexRunner_childEnv:

  • every marker stripped, in both preserveAPIKey / stripAnthropicKey modes
  • user-set CLAUDE_CODE_* config vars and CLAUDE_CONFIG_DIR survive

Var names are spelled out literally in the tests rather than looped over sessionEnvVars, so removing an entry from the production list fails the test instead of agreeing with itself.

make fmt clean, make lint 0 issues, make test passes (race, full suite, 87.9% total coverage).

One caveat worth stating

The set is "every session-scoped marker", not a bisected minimal one. I verified that all-stripped works and all-present reproduces the wedge; I did not test each var individually, since the nested-session detection lives in Claude Code rather than here. CLAUDE_PID and CLAUDE_EFFORT are the two most likely to be unnecessary — happy to drop them if you'd rather keep the list tight. My reasoning for the wider net is that a wrapper is better off dropping anything session-scoped, including markers added in future releases.

AI-assisted, reviewed by me before submitting; I can explain any line.

Stripping CLAUDECODE alone stopped being enough once Claude Code 2.1.x began
marking spawned processes with a family of per-session vars. Any one of them
left in the child env makes the spawned claude behave as a nested session: it
attaches to the parent's session plumbing and never writes its own transcript,
so a PTY wrapper like fya waits for output that never arrives and each turn
burns the full --turn-timeout (30m by default) having done no work and left no
diagnostic beyond the timeout itself.

Observed running ralphex from inside a Claude Code session on Claude Code
2.1.228: /proc/<pid>/environ on the fya child showed CLAUDECODE correctly
removed but CLAUDE_CODE_CHILD_SESSION, CLAUDE_CODE_SESSION_ID,
CLAUDE_CODE_MESSAGING_SOCKET, CLAUDE_CODE_BRIDGE_SESSION_ID and friends still
set. Iterations timed out at 30m each with no transcript written; relaunching
with the full set scrubbed produced a transcript in ~85s and normal progress.

Collect the markers in sessionEnvVars and strip them in both executors -
claudeChildEnv and the codex runner's childEnv have the same defect, and the
codex loop is reached in the same run. The list is explicit rather than a
CLAUDE_CODE_* prefix strip because user-set config vars share that prefix
(CLAUDE_CODE_USE_BEDROCK, _USE_VERTEX, _MAX_OUTPUT_TOKENS, _SUBAGENT_MODEL)
and must still reach the child; a test covers that.

The set is "every session-scoped marker" rather than a bisected minimal one -
which var trips the nested-session detection is Claude Code's business, and a
wrapper is better off dropping all of them, including any added later.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UhHLFUDbCTxtsg4zis4aP4
@alekb
alekb requested a review from umputun as a code owner August 12, 2026 17:10
@umputun

umputun commented Aug 12, 2026

Copy link
Copy Markdown
Owner

lgtm

on CLAUDE_PID and CLAUDE_EFFORT, keep them. Your reasoning is right, a wrapper is better off dropping anything session-scoped, and the cost of an extra entry is nothing.

umputun added a commit that referenced this pull request Aug 13, 2026
execCustomRunner.Run never assigns cmd.Env, so a custom external review
script inherits every Claude Code session marker. The two sibling runners
in pkg/executor both filter. Surfaced reviewing PR #430; predates it, so
left out of that change.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants