diff --git a/src/claude_agent_sdk/_internal/transport/subprocess_cli.py b/src/claude_agent_sdk/_internal/transport/subprocess_cli.py index 58abc438d..4d6cc430a 100644 --- a/src/claude_agent_sdk/_internal/transport/subprocess_cli.py +++ b/src/claude_agent_sdk/_internal/transport/subprocess_cli.py @@ -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: diff --git a/tests/test_transport.py b/tests/test_transport.py index 2a0ed35bf..a81725cc5 100644 --- a/tests/test_transport.py +++ b/tests/test_transport.py @@ -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