Skip to content
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
68 changes: 62 additions & 6 deletions src/claude_agent_sdk/_internal/transport/subprocess_cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
import re
import shutil
import signal
import tempfile
from collections.abc import AsyncIterable, AsyncIterator
from contextlib import suppress
from pathlib import Path
Expand Down Expand Up @@ -76,6 +77,7 @@ def __init__(
if options.max_buffer_size is not None
else _DEFAULT_MAX_BUFFER_SIZE
)
self._temp_files: list[str] = [] # Track temporary files for cleanup
self._write_lock: anyio.Lock = anyio.Lock()

def _find_cli(self) -> str:
Expand Down Expand Up @@ -321,12 +323,28 @@ def _build_command(self) -> list[str]:

# Pass all servers to CLI
if servers_for_cli:
cmd.extend(
[
"--mcp-config",
json.dumps({"mcpServers": servers_for_cli}),
]
)
mcp_config_json = json.dumps({"mcpServers": servers_for_cli})

# On Windows, write the MCP config to a temporary file
# instead of passing it as inline JSON on the command
# line. This avoids command-line argument quoting and
# length issues (see #245 for the same approach with
# --agents; also addresses #902).
if platform.system() == "Windows":
# ruff: noqa: SIM115
tmp = tempfile.NamedTemporaryFile(
mode="w", suffix=".json", delete=False, encoding="utf-8"
)
tmp.write(mcp_config_json)
tmp.close()
self._temp_files.append(tmp.name)
cmd.extend(["--mcp-config", tmp.name])
logger.debug(
"Wrote MCP config to temp file for Windows: %s",
tmp.name,
)
else:
cmd.extend(["--mcp-config", mcp_config_json])
else:
# String or Path format: pass directly as file path or JSON string
cmd.extend(["--mcp-config", str(self._options.mcp_servers)])
Expand Down Expand Up @@ -407,6 +425,20 @@ def _build_command(self) -> list[str]:
# This allows agents and other large configs to be sent via initialize request
cmd.extend(["--input-format", "stream-json"])

# On Windows, ensure the command line doesn't exceed the platform
# limit (8191 chars for CreateProcess). If it does, the subprocess
# creation will fail with "command line too long". Individual
# large-value flags (like --mcp-config) are handled inline above;
# this is a safety net for cumulative length from many flags.
if platform.system() == "Windows":
cmd_str = " ".join(cmd)
if len(cmd_str) > 8000:
logger.warning(
"Command line length (%d) approaches Windows limit (8191). "
"Consider reducing the number or size of options.",
len(cmd_str),
)

return cmd

async def connect(self) -> None:
Expand All @@ -428,6 +460,23 @@ async def connect(self) -> None:
# Filter out CLAUDECODE so SDK-spawned subprocesses don't think
# they're running inside a Claude Code parent (see #573).
inherited_env = {k: v for k, v in os.environ.items() if k != "CLAUDECODE"}

# On Windows, environment variable names are case-insensitive but
# the env block passed to CreateProcess preserves the casing of dict
# keys. If the inherited environment happens to have an SDK-key like
# CLAUDE_CODE_ENTRYPOINT with different casing (e.g.
# "claude_code_entrypoint"), Python's dict merge treats them as
# distinct keys, and Windows' GetEnvironmentVariable may return the
# inherited entry instead of the SDK-controlled value. Strip any
# inherited entries whose case-insensitive key matches an SDK key so
# that the SDK's version is the sole entry in the env block.
if platform.system() == "Windows":
_sdk_keys = {"CLAUDE_CODE_ENTRYPOINT", "CLAUDE_AGENT_SDK_VERSION"}
_sdk_keys_upper = {k.upper() for k in _sdk_keys}
for key in list(inherited_env):
if key.upper() in _sdk_keys_upper:
del inherited_env[key]

process_env = {
**inherited_env,
"CLAUDE_CODE_ENTRYPOINT": "sdk-py",
Expand Down Expand Up @@ -544,6 +593,13 @@ async def _handle_stderr(self) -> None:

async def close(self) -> None:
"""Close the transport and clean up resources."""
# Clean up temporary files (e.g., MCP config files written on Windows
# to work around command-line length limits).
for temp_file in self._temp_files:
with suppress(Exception):
Path(temp_file).unlink(missing_ok=True)
self._temp_files.clear()

if not self._process:
self._ready = False
return
Expand Down