Skip to content

discussion: ClaudeAgentOptions.env only supports implicit parent-env inheritance with no documented isolated environment mode #934

@httpsVishu

Description

@httpsVishu

hey, i was exploring src/claude_agent_sdk/_internal/transport/subprocess_cli.py to understand how the SDK spawns the Claude Code CLI subprocess and noticed that connect() always builds the subprocess environment by inheriting the full parent os.environ and then merging user provided ClaudeAgentOptions.env on top of it

inherited_env = {k: v for k, v in os.environ.items() if k != "CLAUDECODE"}

process_env = {
    **inherited_env,
    "CLAUDE_CODE_ENTRYPOINT": "sdk-py",
    **self._options.env,
    "CLAUDE_AGENT_SDK_VERSION": __version__,
}

at a normal usage level this makes sense because env={"MY_VAR": "value"} works as an override/addition mechanism, the confusing part is that this inheritance behaviour does not seem to be explicitly documented on ClaudeAgentOptions.env and there also does not appear to be a supported way to request a clean or isolated subprocess environment
this looks related to #573 but in this case the concern is broader as users currently get implicit full inheritance but no explicit way to choose between merge with parent env and isolated/clean subprocess env

Impact

due to this, users cannot create a deterministic or sandboxed subprocess environment and also, sensitive parent variables may be inherited unintentionally and CI/reproducibility use cases become harder
(main point: current behaviour may be intentional but the API contract is not explicit enough to make that clear)

How to reproduce

Reproduction
import os
import anyio

from claude_agent_sdk import ClaudeAgentOptions
from claude_agent_sdk._errors import CLIConnectionError
from claude_agent_sdk._internal.transport.subprocess_cli import SubprocessCLITransport


class EnvCaptured(Exception):
    pass


async def fake_open_process(*args, **kwargs):
    env = kwargs["env"]

    print("MY_CUSTOM_VAR:", env.get("MY_CUSTOM_VAR"))
    print("HOME inherited:", "HOME" in env)
    print("PATH inherited:", "PATH" in env)
    print("ANTHROPIC_API_KEY inherited:", "ANTHROPIC_API_KEY" in env)

    raise EnvCaptured("Stopped after capturing subprocess env")


async def fake_check_claude_version():
    return None


async def main():
    os.environ["ANTHROPIC_API_KEY"] = "dummy-secret"

    options = ClaudeAgentOptions(
        env={"MY_CUSTOM_VAR": "hello"},
        max_turns=1,
    )

    transport = SubprocessCLITransport(prompt="hello", options=options)

    transport._cli_path = "dummy-claude"
    transport._build_command = lambda: ["dummy-claude"]
    transport._check_claude_version = fake_check_claude_version

    original_open_process = anyio.open_process
    anyio.open_process = fake_open_process

    try:
        await transport.connect()
    except CLIConnectionError as e:
        if "Stopped after capturing subprocess env" in str(e):
            print("Captured env successfully; reproduction complete.")
        else:
            raise
    finally:
        anyio.open_process = original_open_process


anyio.run(main)

Output:
Image

was the current inheritance-only behaviour intentional or is the lack of an isolated environment mode an oversight? happy to make a pr if this direction sounds useful

Metadata

Metadata

Assignees

No one assigned

    Labels

    documentationImprovements or additions to documentationenhancementNew feature or requestquestionFurther information is requested

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions