Compare commits

...

2 Commits

Author SHA1 Message Date
J
2b24044701 fix(agent): keep CLAUDE_CODE_TMPDIR in child env
CLAUDE_CODE_TMPDIR is a documented, user-configurable temp-dir override
(public env-vars reference), not an internal per-session marker. Claude
Code creates its own per-session subdir under it, so inheriting it is
harmless — and stripping it would silently break a user's temp-dir
override the same way the broad prefix filter broke CLAUDE_CODE_GIT_BASH_PATH.

Drop it from the internal denylist (which now holds only the undocumented
per-process runtime markers: CLAUDECODE, CLAUDE_CODE_ENTRYPOINT,
CLAUDE_CODE_EXECPATH, CLAUDE_CODE_SESSION_ID, CLAUDE_CODE_SSE_PORT) and
assert it reaches the child.

MUL-2940

Co-authored-by: multica-agent <github@multica.ai>
2026-06-03 13:46:04 +08:00
J
2761e92943 fix(agent): stop stripping user-facing CLAUDE_CODE_* config from child env
isFilteredChildEnvKey blanket-removed every CLAUDE_CODE_* var from the
spawned Claude Code child's environment. The intent was only to keep the
daemon's internal session markers from leaking, but CLAUDE_CODE_* is also
Anthropic's user-facing config namespace. On Windows this stripped the
user-set CLAUDE_CODE_GIT_BASH_PATH, so Claude Code could not locate
bash.exe, exited immediately, and every task failed with
"write claude input: write |1: The pipe has been ended."

Switch from prefixing the whole CLAUDE_CODE_ namespace to an exact-name
denylist of the internal runtime/session markers (CLAUDECODE,
CLAUDE_CODE_ENTRYPOINT, CLAUDE_CODE_EXECPATH, CLAUDE_CODE_SESSION_ID,
CLAUDE_CODE_TMPDIR, CLAUDE_CODE_SSE_PORT), still blanket-stripping the
wholly-internal CLAUDECODE_* namespace. Every other CLAUDE_CODE_* var
(GIT_BASH_PATH, USE_BEDROCK, USE_VERTEX, MAX_OUTPUT_TOKENS, ...) now
reaches the child. The internal-marker set was confirmed against the live
runtime, not guessed.

Fixes the whole class, not just git-bash: Bedrock/Vertex/etc. were
silently dropped the same way.

MUL-2940

Co-authored-by: multica-agent <github@multica.ai>
2026-06-03 12:42:52 +08:00
2 changed files with 61 additions and 5 deletions

View File

@@ -590,10 +590,36 @@ func mergeEnv(base []string, extra map[string]string) []string {
return env
}
// isFilteredChildEnvKey reports whether an inherited env var is an internal
// Claude Code runtime/session marker that must NOT leak into the spawned child
// (otherwise the child mistakes itself for a nested or resumed session, or
// inherits the parent's exec path / transport).
//
// It must NOT strip the user-facing CLAUDE_CODE_* configuration namespace
// (CLAUDE_CODE_GIT_BASH_PATH, CLAUDE_CODE_USE_BEDROCK, CLAUDE_CODE_USE_VERTEX,
// CLAUDE_CODE_MAX_OUTPUT_TOKENS, CLAUDE_CODE_TMPDIR, ...): users set those
// deliberately and the child needs them. Blanket-stripping the whole prefix is
// what broke Windows — CLAUDE_CODE_GIT_BASH_PATH was silently removed, so Claude
// Code could not find bash.exe and exited immediately. Strip internal markers by
// exact name and let every other CLAUDE_CODE_* var through.
//
// The denylist holds only undocumented, per-process runtime markers. Anything in
// the public env-vars reference (https://code.claude.com/docs/en/env-vars) is
// user config and stays out of this list — including CLAUDE_CODE_TMPDIR, a
// documented temp-dir override under which Claude Code creates its own
// per-session subdir, so inheriting it is harmless.
func isFilteredChildEnvKey(key string) bool {
return key == "CLAUDECODE" ||
strings.HasPrefix(key, "CLAUDECODE_") ||
strings.HasPrefix(key, "CLAUDE_CODE_")
switch key {
case "CLAUDECODE", // "1" when running inside Claude Code
"CLAUDE_CODE_ENTRYPOINT", // entrypoint marker (cli/sdk-cli/...)
"CLAUDE_CODE_EXECPATH", // path to the running CLI binary
"CLAUDE_CODE_SESSION_ID", // per-session identifier
"CLAUDE_CODE_SSE_PORT": // IDE-extension transport port
return true
}
// CLAUDECODE_* (no underscore between CLAUDE and CODE) is wholly internal;
// keep stripping it. The user-facing config namespace is CLAUDE_CODE_*.
return strings.HasPrefix(key, "CLAUDECODE_")
}
// blockedArgMode specifies whether a blocked arg takes a value or is standalone.

View File

@@ -417,12 +417,29 @@ func TestMergeEnvFiltersClaudeCodeVars(t *testing.T) {
"PATH=/usr/bin",
"CLAUDECODE=1",
"CLAUDE_CODE_ENTRYPOINT=cli",
"CLAUDE_CODE_EXECPATH=/opt/claude",
"CLAUDE_CODE_SESSION_ID=abc123",
"CLAUDE_CODE_SSE_PORT=9999",
"CLAUDECODEX=keep-me",
"CLAUDE_CODE_GIT_BASH_PATH=C:\\Program Files\\Git\\bin\\bash.exe",
"CLAUDE_CODE_USE_BEDROCK=1",
"CLAUDE_CODE_TMPDIR=/custom/tmp",
}, map[string]string{"FOO": "bar"})
// Internal runtime/session markers must be stripped so the child does not
// inherit the parent's identity or transport.
filteredOut := []string{
"CLAUDECODE=1",
"CLAUDE_CODE_ENTRYPOINT=cli",
"CLAUDE_CODE_EXECPATH=/opt/claude",
"CLAUDE_CODE_SESSION_ID=abc123",
"CLAUDE_CODE_SSE_PORT=9999",
}
for _, entry := range env {
if entry == "CLAUDECODE=1" || entry == "CLAUDE_CODE_ENTRYPOINT=cli" {
t.Fatalf("expected CLAUDECODE vars to be filtered, got %v", env)
for _, banned := range filteredOut {
if entry == banned {
t.Fatalf("expected internal Claude Code marker %q to be filtered, got %v", banned, env)
}
}
}
@@ -437,6 +454,19 @@ func TestMergeEnvFiltersClaudeCodeVars(t *testing.T) {
if !found["CLAUDECODEX=keep-me"] {
t.Fatalf("expected unrelated env vars to be preserved, got %v", env)
}
// User-facing CLAUDE_CODE_* config must reach the child — stripping
// CLAUDE_CODE_GIT_BASH_PATH is what broke Claude Code on Windows (#3671).
if !found["CLAUDE_CODE_GIT_BASH_PATH=C:\\Program Files\\Git\\bin\\bash.exe"] {
t.Fatalf("expected CLAUDE_CODE_GIT_BASH_PATH to be preserved, got %v", env)
}
if !found["CLAUDE_CODE_USE_BEDROCK=1"] {
t.Fatalf("expected CLAUDE_CODE_USE_BEDROCK to be preserved, got %v", env)
}
// CLAUDE_CODE_TMPDIR is a documented user-configurable temp-dir override, not
// an internal per-session marker, so it must reach the child.
if !found["CLAUDE_CODE_TMPDIR=/custom/tmp"] {
t.Fatalf("expected CLAUDE_CODE_TMPDIR to be preserved, got %v", env)
}
if !found["FOO=bar"] {
t.Fatalf("expected extra env var to be appended, got %v", env)
}