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
34 changes: 14 additions & 20 deletions src/claude_agent_sdk/_internal/transport/subprocess_cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -486,29 +486,23 @@ def _build_settings_value(self) -> str | None:
if has_settings:
assert self._options.settings is not None
settings_str = self._options.settings.strip()
# Check if settings is a JSON string or a file path
if settings_str.startswith("{") and settings_str.endswith("}"):
# Parse JSON string
try:
settings_obj = json.loads(settings_str)
except json.JSONDecodeError:
# If parsing fails, treat as file path
logger.warning(
f"Failed to parse settings as JSON, treating as file path: {settings_str}"
)
# Read the file
settings_path = Path(settings_str)
if settings_path.exists():
with settings_path.open(encoding="utf-8") as f:
settings_obj = json.load(f)
# Explicitly separate JSON string vs. file path branches.
# A value that starts with '{' is always treated as inline JSON;
# parse failures are raised immediately instead of falling back
# to file-path interpretation (which could read unintended files
# when the value is e.g. "{bad json}").
if settings_str.startswith("{"):
# Parse as JSON string — propagate errors to the caller
settings_obj = json.loads(settings_str)
else:
# It's a file path - read and parse
settings_path = Path(settings_str)
if settings_path.exists():
with settings_path.open(encoding="utf-8") as f:
settings_obj = json.load(f)
else:
logger.warning(f"Settings file not found: {settings_path}")
if not settings_path.exists():
raise FileNotFoundError(
f"Settings file not found: {settings_str}"
)
with settings_path.open(encoding="utf-8") as f:
settings_obj = json.load(f)

# Merge sandbox settings
if has_sandbox:
Expand Down
63 changes: 63 additions & 0 deletions tests/test_transport.py
Original file line number Diff line number Diff line change
Expand Up @@ -1821,6 +1821,69 @@ def test_build_command_with_settings_file_and_no_sandbox(self):
settings_idx = cmd.index("--settings")
assert cmd[settings_idx + 1] == "/path/to/settings.json"

def test_build_command_with_invalid_settings_json_raises_decode_error(self):
"""Test that malformed JSON in settings raises JSONDecodeError and does not fallback to file path."""
import json

from claude_agent_sdk import SandboxSettings

sandbox: SandboxSettings = {"enabled": True}
transport = SubprocessCLITransport(
prompt="test",
options=make_options(settings="{invalid json: true}", sandbox=sandbox),
)

with pytest.raises(json.JSONDecodeError):
transport._build_command()

def test_build_command_with_sandbox_and_settings_file(self, tmp_path):
"""Test building CLI command with sandbox merged into existing settings file."""
import json

from claude_agent_sdk import SandboxSettings

settings_file = tmp_path / "settings.json"
settings_file.write_text(
'{"permissions": {"allow": ["Bash(ls:*)"]}, "verbose": true}',
encoding="utf-8",
)

sandbox: SandboxSettings = {
"enabled": True,
"excludedCommands": ["git"],
}

transport = SubprocessCLITransport(
prompt="test",
options=make_options(settings=str(settings_file), sandbox=sandbox),
)

cmd = transport._build_command()

assert "--settings" in cmd
settings_idx = cmd.index("--settings")
settings_value = cmd[settings_idx + 1]

parsed = json.loads(settings_value)
assert parsed["permissions"] == {"allow": ["Bash(ls:*)"]}
assert parsed["verbose"] is True
assert parsed["sandbox"] == {"enabled": True, "excludedCommands": ["git"]}

def test_build_command_with_nonexistent_settings_file_raises_error(self):
"""Test that nonexistent settings file raises FileNotFoundError when merged with sandbox."""
from claude_agent_sdk import SandboxSettings

sandbox: SandboxSettings = {"enabled": True}
transport = SubprocessCLITransport(
prompt="test",
options=make_options(
settings="/nonexistent/path/to/settings.json", sandbox=sandbox
),
)

with pytest.raises(FileNotFoundError, match="Settings file not found"):
transport._build_command()

def test_build_command_sandbox_minimal(self):
"""Test sandbox with minimal configuration."""
import json
Expand Down