Skip to content

Commit 5ac7a0a

Browse files
committed
fix: prevent internal options from leaking as CLI flags
- Add missing control_timeout nil guard in convert_option - Change catch-all from blindly converting to CLI flags to logging a warning and returning nil - Change extra_args from list to map (flag => value or true for boolean flags) to align with the Python SDK - Add .claude/rules/options.md to remind about keeping Options and CLI.Command in sync
1 parent 27087c4 commit 5ac7a0a

5 files changed

Lines changed: 49 additions & 21 deletions

File tree

.claude/rules/options.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
---
2+
paths:
3+
- "lib/claude_code/options.ex"
4+
---
5+
6+
When adding, editing, or removing options from the session schema, update the corresponding `convert_option` clause in `lib/claude_code/cli/command.ex`. Options that are not CLI flags need a `defp convert_option(:option_name, _value), do: nil` clause to prevent them from leaking to the CLI.

lib/claude_code/cli/command.ex

Lines changed: 20 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@ defmodule ClaudeCode.CLI.Command do
1111

1212
alias ClaudeCode.Session.PermissionMode
1313

14+
require Logger
15+
1416
@required_flags ["--output-format", "stream-json", "--verbose", "--print"]
1517

1618
@doc """
@@ -69,7 +71,7 @@ defmodule ClaudeCode.CLI.Command do
6971
nil -> acc
7072
end
7173
end)
72-
|> Enum.reverse(Keyword.get(opts, :extra_args, []))
74+
|> Enum.reverse(build_extra_args(Keyword.get(opts, :extra_args, %{})))
7375
end
7476

7577
# -- Private: option conversion ---------------------------------------------
@@ -358,6 +360,7 @@ defmodule ClaudeCode.CLI.Command do
358360
defp convert_option(:sandbox, _value), do: nil
359361
defp convert_option(:thinking, _value), do: nil
360362
defp convert_option(:enable_file_checkpointing, _value), do: nil
363+
defp convert_option(:control_timeout, _value), do: nil
361364
defp convert_option(:callers, _value), do: nil
362365
defp convert_option(:stub_name, _value), do: nil
363366
defp convert_option(:env, _value), do: nil
@@ -367,10 +370,22 @@ defmodule ClaudeCode.CLI.Command do
367370
defp convert_option(:prompt_suggestions, _value), do: nil
368371
defp convert_option(:tool_config, _value), do: nil
369372

370-
defp convert_option(key, value) do
371-
# Convert unknown keys to kebab-case flags
372-
flag_name = "--" <> (key |> to_string() |> String.replace("_", "-"))
373-
{flag_name, to_string(value)}
373+
# If this fires, a new option was added to Options but not handled here.
374+
defp convert_option(key, _value) do
375+
Logger.warning("ClaudeCode.CLI.Command: unknown option #{inspect(key)} — needs a convert_option clause")
376+
377+
nil
378+
end
379+
380+
# -- Private: extra args conversion ------------------------------------------
381+
382+
defp build_extra_args(extra_args) when map_size(extra_args) == 0, do: []
383+
384+
defp build_extra_args(extra_args) do
385+
Enum.flat_map(extra_args, fn
386+
{flag, true} -> [flag]
387+
{flag, value} -> [flag, value]
388+
end)
374389
end
375390

376391
# -- Private: setting sources preprocessing ----------------------------------

lib/claude_code/options.ex

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -133,7 +133,7 @@ defmodule ClaudeCode.Options do
133133
| `cli_path` | atom/string | `:bundled` | CLI binary resolution: `:bundled` (auto-install), `:global` (system), or explicit path |
134134
| `file` | list | - | File resources to download at startup (`"file_id:relative_path"` format) |
135135
| `enable_file_checkpointing` | boolean | false | Track file changes for rewinding. See [File Checkpointing](file-checkpointing.md) |
136-
| `extra_args` | list | `[]` | Additional CLI arguments passed directly to the binary |
136+
| `extra_args` | map | `%{}` | Additional CLI arguments passed directly to the binary (flag → value or `true` for boolean flags) |
137137
138138
### Debugging
139139
@@ -533,10 +533,10 @@ defmodule ClaudeCode.Options do
533533
~s|Per-tool configuration for built-in tools. Map of tool name to config map (e.g. %{"askUserQuestion" => %{"previewFormat" => "html"}})|
534534
],
535535
extra_args: [
536-
type: {:list, :string},
537-
default: [],
536+
type: {:map, :string, {:or, [:string, {:in, [true]}]}},
537+
default: %{},
538538
doc:
539-
~s{Additional CLI arguments passed directly to the claude binary. Each element is a separate argument (e.g. ["--flag", "value"]).}
539+
~s|Additional CLI arguments passed directly to the claude binary. Map of flag name to value string, or `true` for boolean flags (e.g. %{"--some-flag" => "value", "--bool-flag" => true}).|
540540
],
541541
max_buffer_size: [
542542
type: :pos_integer,

test/claude_code/cli/command_test.exs

Lines changed: 14 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1089,28 +1089,35 @@ defmodule ClaudeCode.CLI.CommandTest do
10891089
refute "--enable-file-checkpointing" in args
10901090
end
10911091

1092-
test "extra_args are appended at end of CLI args" do
1093-
opts = [model: "opus", extra_args: ["--new-flag", "value"]]
1092+
test "extra_args map with value is appended at end of CLI args" do
1093+
opts = [model: "opus", extra_args: %{"--new-flag" => "value"}]
10941094

10951095
args = Command.to_cli_args(opts)
10961096
assert "--model" in args
10971097
assert "opus" in args
1098-
assert List.last(args) == "value"
1099-
assert Enum.at(args, -2) == "--new-flag"
1098+
assert "--new-flag" in args
1099+
assert "value" in args
1100+
end
1101+
1102+
test "extra_args map with boolean flag" do
1103+
opts = [extra_args: %{"--bool-flag" => true}]
1104+
1105+
args = Command.to_cli_args(opts)
1106+
assert "--bool-flag" in args
1107+
refute "true" in args
11001108
end
11011109

11021110
test "empty extra_args produces no extra arguments beyond options" do
1103-
opts = [model: "opus", extra_args: []]
1111+
opts = [model: "opus", extra_args: %{}]
11041112

11051113
args = Command.to_cli_args(opts)
11061114
assert "--model" in args
11071115
assert "opus" in args
1108-
# --setting-sources "" is always added by ensure_setting_sources
11091116
refute "--extra-args" in args
11101117
end
11111118

11121119
test "extra_args appear after all converted options" do
1113-
opts = [model: "opus", max_turns: 10, extra_args: ["--custom"]]
1120+
opts = [model: "opus", max_turns: 10, extra_args: %{"--custom" => true}]
11141121

11151122
args = Command.to_cli_args(opts)
11161123
custom_pos = Enum.find_index(args, &(&1 == "--custom"))

test/claude_code/options_test.exs

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -462,16 +462,16 @@ defmodule ClaudeCode.OptionsTest do
462462
assert validated[:enable_file_checkpointing] == false
463463
end
464464

465-
test "validates extra_args option as list of strings" do
466-
opts = [extra_args: ["--some-flag", "value"]]
465+
test "validates extra_args option as map" do
466+
opts = [extra_args: %{"--some-flag" => "value", "--bool-flag" => true}]
467467
assert {:ok, validated} = Options.validate_session_options(opts)
468-
assert validated[:extra_args] == ["--some-flag", "value"]
468+
assert validated[:extra_args] == %{"--some-flag" => "value", "--bool-flag" => true}
469469
end
470470

471-
test "defaults extra_args to empty list" do
471+
test "defaults extra_args to empty map" do
472472
opts = []
473473
assert {:ok, validated} = Options.validate_session_options(opts)
474-
assert validated[:extra_args] == []
474+
assert validated[:extra_args] == %{}
475475
end
476476

477477
test "validates max_buffer_size option" do

0 commit comments

Comments
 (0)