fix: filter CLAUDECODE env var from subprocess environment#732
fix: filter CLAUDECODE env var from subprocess environment#732
Conversation
There was a problem hiding this comment.
LGTM — straightforward env var filtering with good test coverage.
Extended reasoning...
Overview
This PR modifies SubprocessCLITransport.connect() in subprocess_cli.py to filter the CLAUDECODE environment variable from the inherited os.environ before passing it to the child CLI subprocess. Two new tests in test_transport.py verify both the filtering behavior and the ability to explicitly re-set CLAUDECODE via options.env.
Security risks
No security concerns. The change reduces environment leakage (filtering out an env var that causes unintended behavior), which is a positive security direction. Users retain the ability to explicitly opt in via options.env.
Level of scrutiny
This is a low-risk, minimal change. The production diff is a 3-line dict comprehension plus a 2-line comment. The logic is straightforward — filter a single key from os.environ before merging. The env merge order (inherited_env → SDK defaults → options.env → SDK version) is preserved correctly, ensuring user overrides still work.
Other factors
The previously reported f-string brace escaping issue in the concurrent write tests has been resolved by a follow-up commit (b558fc1). The current test file has correct f-strings. Both new tests follow the same mock pattern used by existing env-related tests in the file. All 305 tests pass per the PR description.
There was a problem hiding this comment.
LGTM — straightforward env var filtering with good test coverage.
Extended reasoning...
Overview
This PR makes a small, targeted change to SubprocessCLITransport.connect() in subprocess_cli.py: it filters the CLAUDECODE environment variable from os.environ before passing it to the child subprocess. Two new tests in test_transport.py verify that (1) CLAUDECODE is not inherited and (2) users can still explicitly set it via options.env.
Security risks
No security concerns. The change reduces unintended env var leakage into subprocesses. The filtering is narrowly scoped to a single known env var, and the override path via options.env is intentional and documented.
Level of scrutiny
This is a low-risk, mechanical fix. The dict comprehension is trivial to verify, the env merge order (inherited → defaults → user options → SDK version) is preserved, and the tests directly assert the expected behavior. The previous inline comment about f-string brace escaping has been resolved.
Other factors
The PR fixes a real issue (#573) where SDK subprocesses misbehave when launched from Claude Code hooks/plugins. The implementation follows the existing pattern in the codebase for env construction. Both new tests follow the same mock structure as the existing test_env_vars_passed_to_subprocess and test_caller_can_override_entrypoint tests.
There was a problem hiding this comment.
LGTM — small, targeted env var filter with good test coverage for the core behavior.
Extended reasoning...
Overview
This PR makes a minimal 3-line production change in subprocess_cli.py to filter CLAUDECODE from the inherited environment when spawning CLI subprocesses. Two new tests are added in test_transport.py to verify the filtering and the options.env override path. The change fixes a real issue (#573) where SDK subprocesses incorrectly inherited CLAUDECODE=1 from parent Claude Code hooks/plugins.
Security risks
No security concerns. The change restricts environment variable inheritance (a defensive measure), and the filtered variable (CLAUDECODE) is a feature flag, not a credential or secret.
Level of scrutiny
Low scrutiny warranted. This is a straightforward environment variable filter using a dict comprehension — a common Python pattern with no ambiguity. The merge order (inherited_env -> defaults -> options.env -> SDK version) is correct and well-commented.
Other factors
The one nit found (test not fully exercising the filter+override interaction) is a valid test quality improvement but does not affect production correctness. The first test (test_claudecode_env_var_not_inherited) properly covers the critical filtering path by injecting CLAUDECODE into os.environ. The previous f-string bug report from my prior run was on pre-existing code not modified by this PR and is already resolved.
E2E Test ResultsTest script: """E2E test for PR #732: Filter CLAUDECODE env var from subprocess environment.
This test sets CLAUDECODE=1 in the parent process env, then uses
claude_agent_sdk.query() to send a simple prompt. Without the fix,
the CLI subprocess would inherit CLAUDECODE=1 and think it's running
inside Claude Code, causing it to malfunction. With the fix, the env
var is filtered out and the query succeeds normally.
"""
import asyncio
import os
import sys
# Set the env var BEFORE importing or using the SDK, simulating a scenario
# where the SDK is used from within a Claude Code extension or similar context.
os.environ["CLAUDECODE"] = "1"
import claude_agent_sdk # noqa: E402
from claude_agent_sdk import ClaudeAgentOptions # noqa: E402
async def main() -> None:
print(f"CLAUDECODE in parent env: {os.environ.get('CLAUDECODE')!r}")
print(f"SDK version: {claude_agent_sdk.__version__}")
print()
print("Sending query with max_turns=1...")
print()
messages = []
try:
async for msg in claude_agent_sdk.query(
prompt="Reply with exactly: hello world",
options=ClaudeAgentOptions(max_turns=1),
):
messages.append(msg)
except Exception as exc:
print(f"FAIL: query raised {type(exc).__name__}: {exc}", file=sys.stderr)
sys.exit(1)
print(f"Num messages: {len(messages)}")
print()
found_hello_world = False
for i, msg in enumerate(messages):
msg_dict = {}
for attr in ["type", "role", "content", "session_id", "message"]:
val = getattr(msg, attr, None)
if val is not None:
if isinstance(val, list):
msg_dict[attr] = [str(v)[:100] for v in val]
else:
msg_dict[attr] = str(val)[:200]
print(f" [{i}] {msg_dict}")
content = getattr(msg, "content", None)
if isinstance(content, str) and "hello world" in content.lower():
found_hello_world = True
elif isinstance(content, list):
for block in content:
text = getattr(block, "text", "")
if "hello world" in text.lower():
found_hello_world = True
print()
if found_hello_world:
print("PASS: Query succeeded with CLAUDECODE=1 in parent env.")
print("The fix correctly filters CLAUDECODE from the subprocess environment.")
else:
all_text = " ".join(str(m) for m in messages)
if "hello world" in all_text.lower():
print("PASS: Query succeeded with CLAUDECODE=1 in parent env.")
else:
print("FAIL: Did not find expected 'hello world' in responses.", file=sys.stderr)
sys.exit(1)
if __name__ == "__main__":
asyncio.run(main())Output: Summary: With |
There was a problem hiding this comment.
LGTM — simple, correct env var filtering with appropriate tests.
Extended reasoning...
Overview
This PR modifies two files: subprocess_cli.py (3-line production change) and test_transport.py (two new test functions). The production change adds a dict comprehension to filter CLAUDECODE from os.environ before passing the environment to the CLI subprocess, preventing it from mistakenly thinking it is running inside a Claude Code parent process. Users can still explicitly set CLAUDECODE via options.env since that dict is spread after the filter.
Security risks
None. The change removes an environment variable from subprocess inheritance rather than adding one. No auth, crypto, or permissions code is affected.
Level of scrutiny
This is a low-risk, small bug fix. The production change is a single dict comprehension with clear semantics. The env merge ordering (inherited_env filtered first, then options.env spread on top) is correct and matches the existing pattern. E2E verification confirms the fix works in practice.
Other factors
My two previous inline comments are both marked resolved. The f-string brace escaping issue was a false positive (those lines are not modified by this PR). The patch.dict no-op in the override test is a minor test quality nit — the production code is correct regardless. The PR has passing E2E tests and all 305 unit tests pass.
- Remove trailing whitespace on line 287 (ruff W291) - Restore double braces in f-strings for concurrent write tests to produce literal JSON output instead of format specifier errors
037826b to
1223bd9
Compare
There was a problem hiding this comment.
LGTM — simple, well-scoped env var filter with test coverage and E2E verification.
Extended reasoning...
Overview
This PR makes a minimal change to subprocess_cli.py: filtering the CLAUDECODE env var from os.environ before passing it to the child subprocess, so SDK-spawned CLI processes don't mistakenly think they're running inside a Claude Code parent. Two new unit tests verify the filtering and the explicit override path via options.env.
Security risks
No security concerns. The change only removes one specific env var from subprocess inheritance. Users can still explicitly set it via options.env if needed.
Level of scrutiny
This is a low-risk, 3-line production code change with a clear motivation (fixes #573). The env merging logic is straightforward dict comprehension + spread. The follow-up commits addressed the f-string brace escaping issue I flagged earlier. E2E verification confirms the fix works end-to-end.
Other factors
Both prior inline comments from my earlier reviews are resolved. The test for options.env override still uses a no-op patch.dict (minor test quality nit, not a production issue), but the production code is correct and the primary filtering test is solid. All 305 unit tests pass, ruff clean.
Summary
When the Python SDK is used inside a Claude Code hook or plugin, the parent process has
CLAUDECODE=1in its environment. The SDK'sSubprocessCLITransport.connect()inherited all ofos.environinto the child subprocess, includingCLAUDECODE. This caused the CLI subprocess to think it was running inside a Claude Code parent, which disables certain features and can cause unexpected behavior.Changes
subprocess_cli.pyFilter out
CLAUDECODEfrom the inherited environment before passing it to the subprocess:Users who explicitly need
CLAUDECODEcan still set it viaoptions.env.test_transport.pyTwo new tests:
test_claudecode_env_var_not_inherited— verifiesCLAUDECODEis filtered from the subprocess environment when present inos.environtest_claudecode_can_be_set_via_options_env— verifies users can still explicitly setCLAUDECODEviaClaudeAgentOptions(env={"CLAUDECODE": "1"})E2E Verification
Verified with a live SDK instance (v0.1.50, CLI 2.1.84):
Test Results
All 305 unit tests pass. Ruff clean.
Supersedes #594 (which has merge conflicts).
Fixes #573