From 399c86f19e1ac1d560153410947345fec200c21e Mon Sep 17 00:00:00 2001 From: chintan-diwakar <48587709+chintan-diwakar@users.noreply.github.com> Date: Sat, 15 Aug 2026 14:24:15 +0530 Subject: [PATCH] fix(transport): default an enabled sandbox to failIfUnavailable The CLI reads sandbox.failIfUnavailable as `?? false`, so when a caller asks for a sandbox and it cannot start (missing dependencies, unsupported platform, unprivileged container), the CLI prints a warning and then runs every bash command unsandboxed. The Python transport passed options.sandbox through verbatim, so callers silently lost the isolation they asked for. The TypeScript SDK closes this: its settings merge injects failIfUnavailable: true whenever sandbox.enabled is true and the caller did not set it. Identical options therefore fail closed in TypeScript and fail open in Python. Default failIfUnavailable to true for an enabled sandbox. An explicit value is always preserved, a sandbox that is not enabled is untouched, and the caller's dict is copied rather than mutated. Add failIfUnavailable to the SandboxSettings TypedDict so an explicit override type-checks. Co-Authored-By: Claude Opus 5 --- .../_internal/transport/subprocess_cli.py | 12 ++- src/claude_agent_sdk/types.py | 5 ++ tests/test_transport.py | 81 ++++++++++++++++++- 3 files changed, 95 insertions(+), 3 deletions(-) diff --git a/src/claude_agent_sdk/_internal/transport/subprocess_cli.py b/src/claude_agent_sdk/_internal/transport/subprocess_cli.py index 58abc438d..6fbdceb95 100644 --- a/src/claude_agent_sdk/_internal/transport/subprocess_cli.py +++ b/src/claude_agent_sdk/_internal/transport/subprocess_cli.py @@ -510,9 +510,17 @@ def _build_settings_value(self) -> str | None: else: logger.warning(f"Settings file not found: {settings_path}") - # Merge sandbox settings + # Merge sandbox settings. The CLI reads sandbox.failIfUnavailable as + # `?? false`, so an enabled sandbox that cannot start degrades to a + # warning and every command then runs unsandboxed. Default it to true + # for an enabled sandbox, matching the TypeScript SDK, so callers who + # asked for a sandbox fail loudly instead of silently losing it. if has_sandbox: - settings_obj["sandbox"] = self._options.sandbox + assert self._options.sandbox is not None + sandbox: dict[str, Any] = dict(self._options.sandbox) + if sandbox.get("enabled") is True and "failIfUnavailable" not in sandbox: + sandbox["failIfUnavailable"] = True + settings_obj["sandbox"] = sandbox return json.dumps(settings_obj) diff --git a/src/claude_agent_sdk/types.py b/src/claude_agent_sdk/types.py index b0f5ce5d5..803c42636 100644 --- a/src/claude_agent_sdk/types.py +++ b/src/claude_agent_sdk/types.py @@ -888,6 +888,10 @@ class SandboxSettings(TypedDict, total=False): Attributes: enabled: Enable bash sandboxing (macOS/Linux only). Default: False + failIfUnavailable: Exit with an error at startup if ``enabled`` is True but the + sandbox cannot start. When False the CLI only warns and commands run + unsandboxed. The SDK defaults this to True for an enabled sandbox, + matching the TypeScript SDK. autoAllowBashIfSandboxed: Auto-approve bash commands when sandboxed. Default: True excludedCommands: Commands that should run outside the sandbox (e.g., ["git", "docker"]) allowUnsandboxedCommands: Allow commands to bypass sandbox via dangerouslyDisableSandbox. @@ -912,6 +916,7 @@ class SandboxSettings(TypedDict, total=False): """ enabled: bool + failIfUnavailable: bool autoAllowBashIfSandboxed: bool excludedCommands: list[str] allowUnsandboxedCommands: bool diff --git a/tests/test_transport.py b/tests/test_transport.py index 2a0ed35bf..a5bb19178 100644 --- a/tests/test_transport.py +++ b/tests/test_transport.py @@ -1841,7 +1841,86 @@ def test_build_command_sandbox_minimal(self): settings_value = cmd[settings_idx + 1] parsed = json.loads(settings_value) - assert parsed == {"sandbox": {"enabled": True}} + assert parsed == {"sandbox": {"enabled": True, "failIfUnavailable": True}} + + def test_sandbox_enabled_defaults_to_fail_closed(self): + """An enabled sandbox defaults failIfUnavailable to True. + + The CLI reads this setting as ``?? false``, so without it a sandbox + that cannot start only warns and every command runs unsandboxed. The + TypeScript SDK injects True here; the Python SDK must match so the + same options do not silently fail open. + """ + import json + + from claude_agent_sdk import SandboxSettings + + sandbox: SandboxSettings = {"enabled": True, "excludedCommands": ["git"]} + + transport = SubprocessCLITransport( + prompt="test", + options=make_options(sandbox=sandbox), + ) + + cmd = transport._build_command() + parsed = json.loads(cmd[cmd.index("--settings") + 1]) + + assert parsed["sandbox"]["failIfUnavailable"] is True + assert parsed["sandbox"]["excludedCommands"] == ["git"] + + def test_sandbox_explicit_fail_if_unavailable_is_preserved(self): + """An explicit failIfUnavailable is never overridden.""" + import json + + from claude_agent_sdk import SandboxSettings + + sandbox: SandboxSettings = {"enabled": True, "failIfUnavailable": False} + + transport = SubprocessCLITransport( + prompt="test", + options=make_options(sandbox=sandbox), + ) + + cmd = transport._build_command() + parsed = json.loads(cmd[cmd.index("--settings") + 1]) + + assert parsed["sandbox"]["failIfUnavailable"] is False + + def test_sandbox_not_enabled_gets_no_fail_if_unavailable(self): + """A sandbox that is not enabled is passed through untouched.""" + import json + + from claude_agent_sdk import SandboxSettings + + sandbox: SandboxSettings = {"enabled": False} + + transport = SubprocessCLITransport( + prompt="test", + options=make_options(sandbox=sandbox), + ) + + cmd = transport._build_command() + parsed = json.loads(cmd[cmd.index("--settings") + 1]) + + assert parsed == {"sandbox": {"enabled": False}} + + def test_sandbox_options_dict_is_not_mutated(self): + """Building the command does not mutate the caller's sandbox dict.""" + import json + + from claude_agent_sdk import SandboxSettings + + sandbox: SandboxSettings = {"enabled": True} + + transport = SubprocessCLITransport( + prompt="test", + options=make_options(sandbox=sandbox), + ) + + cmd = transport._build_command() + json.loads(cmd[cmd.index("--settings") + 1]) + + assert sandbox == {"enabled": True} def test_sandbox_network_config(self): """Test sandbox with full network configuration."""