Skip to content
Open
Show file tree
Hide file tree
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
12 changes: 10 additions & 2 deletions src/claude_agent_sdk/_internal/transport/subprocess_cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand Down
5 changes: 5 additions & 0 deletions src/claude_agent_sdk/types.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -912,6 +916,7 @@ class SandboxSettings(TypedDict, total=False):
"""

enabled: bool
failIfUnavailable: bool
autoAllowBashIfSandboxed: bool
excludedCommands: list[str]
allowUnsandboxedCommands: bool
Expand Down
81 changes: 80 additions & 1 deletion tests/test_transport.py
Original file line number Diff line number Diff line change
Expand Up @@ -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."""
Expand Down