claude-agent-sdk 1.1.0 → 1.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (46) hide show
  1. checksums.yaml +4 -4
  2. data/.yardopts +10 -0
  3. data/CHANGELOG.md +90 -0
  4. data/README.md +43 -31
  5. data/docs/cli-installer.md +26 -4
  6. data/docs/client.md +29 -11
  7. data/docs/configuration.md +164 -1
  8. data/docs/errors.md +32 -2
  9. data/docs/hooks-and-permissions.md +30 -10
  10. data/docs/mcp-servers.md +30 -9
  11. data/docs/observability.md +61 -10
  12. data/docs/options.md +232 -0
  13. data/docs/rails.md +263 -18
  14. data/docs/sessions.md +40 -12
  15. data/docs/subagents.md +1 -1
  16. data/docs/types.md +100 -11
  17. data/lib/claude_agent_sdk/cli_installer.rb +140 -19
  18. data/lib/claude_agent_sdk/command_builder.rb +84 -27
  19. data/lib/claude_agent_sdk/fiber_boundary.rb +45 -2
  20. data/lib/claude_agent_sdk/instrumentation/otel.rb +90 -28
  21. data/lib/claude_agent_sdk/query.rb +228 -77
  22. data/lib/claude_agent_sdk/railtie.rb +27 -2
  23. data/lib/claude_agent_sdk/sdk_mcp_server.rb +78 -26
  24. data/lib/claude_agent_sdk/session_mutations.rb +112 -92
  25. data/lib/claude_agent_sdk/session_resume.rb +356 -39
  26. data/lib/claude_agent_sdk/session_store.rb +31 -2
  27. data/lib/claude_agent_sdk/sessions.rb +720 -138
  28. data/lib/claude_agent_sdk/subprocess_cli_transport.rb +227 -29
  29. data/lib/claude_agent_sdk/testing/session_store_conformance.rb +18 -7
  30. data/lib/claude_agent_sdk/transcript_mirror_batcher.rb +45 -37
  31. data/lib/claude_agent_sdk/transport.rb +28 -12
  32. data/lib/claude_agent_sdk/types/attributes.rb +9 -0
  33. data/lib/claude_agent_sdk/types/base.rb +85 -15
  34. data/lib/claude_agent_sdk/types/hooks.rb +73 -0
  35. data/lib/claude_agent_sdk/types/mcp.rb +37 -1
  36. data/lib/claude_agent_sdk/types/option_values.rb +186 -4
  37. data/lib/claude_agent_sdk/types/options.rb +35 -5
  38. data/lib/claude_agent_sdk/types/permissions.rb +18 -9
  39. data/lib/claude_agent_sdk/version.rb +1 -1
  40. data/lib/claude_agent_sdk.rb +94 -46
  41. data/lib/generators/claude_agent_sdk/install/templates/claude_agent_sdk.rb.tt +6 -0
  42. data/sig/claude_agent_sdk/types/hooks.rbs +6 -3
  43. data/sig/claude_agent_sdk/types/option_values.rbs +23 -6
  44. data/sig/claude_agent_sdk/types/options.rbs +11 -7
  45. data/sig/claude_agent_sdk/types/permissions.rbs +4 -2
  46. metadata +6 -4
@@ -18,8 +18,10 @@ module ClaudeAgentSDK
18
18
 
19
19
  # Claude Agent Options for configuring queries
20
20
  class ClaudeAgentOptions < Type
21
- # `env` routinely carries credentials (ANTHROPIC_API_KEY, ...).
22
- inspect_filtered :env
21
+ # `env` routinely carries credentials (ANTHROPIC_API_KEY, ...). So does
22
+ # `settings` (its own `env`, `apiKeyHelper`), as a Hash or a JSON String,
23
+ # and an `extra_args` value can be one.
24
+ inspect_filtered :env, :settings, :extra_args
23
25
 
24
26
  attr_accessor :allowed_tools, :system_prompt, :mcp_servers, :permission_mode,
25
27
  :resume, :resume_session_at, :session_id, :max_turns, :disallowed_tools,
@@ -345,16 +347,44 @@ module ClaudeAgentSDK
345
347
  self.callback_scheduling = :thread if callback_scheduling.nil?
346
348
  end
347
349
 
350
+ # `mcp_servers` holds typed configs, which filter themselves, next to raw
351
+ # Hash configs carrying the same credentials: those are rendered by
352
+ # Type#inspect_mcp_server_config. Anything but a Hash (the JSON of a
353
+ # config as a String, or the path to one) is replaced outright, like a
354
+ # String `settings`.
355
+ def inspect_attributes
356
+ super.map { |name, value| [name, name == 'mcp_servers' ? inspect_mcp_servers(value) : value] }
357
+ end
358
+
359
+ def inspect_mcp_servers(servers)
360
+ return '[FILTERED]' unless servers.is_a?(Hash)
361
+
362
+ servers.to_h { |name, config| [name, config.is_a?(Hash) ? inspect_mcp_server_config(config) : config] }
363
+ rescue StandardError
364
+ '[FILTERED]'
365
+ end
366
+
348
367
  # Strict key validation: unlike other Type subclasses (which silently drop
349
368
  # unknown keys for forward-compat with newer CLI output), ClaudeAgentOptions
350
369
  # is a developer-facing config object — typos should fail loudly.
351
370
  def assign_attribute(name, value)
352
- setter = :"#{normalize_name(name)}="
353
- raise ArgumentError, "unknown ClaudeAgentOptions option: #{name.inspect}" unless respond_to?(setter)
371
+ setter = option_setter(normalize_name(name))
372
+ raise ArgumentError, "unknown ClaudeAgentOptions option: #{name.inspect}" unless setter
354
373
 
355
374
  public_send(setter, value)
356
375
  end
357
376
 
377
+ # The writer of the option with this normalized name, nil when the name
378
+ # is not an option. An option is a declared attribute, or a setter user
379
+ # code defined (on a subclass, a module it includes, or the object);
380
+ # respond_to? alone would also answer for '[]' (#[]=) and '=' (#==).
381
+ def option_setter(normalized)
382
+ setter = :"#{normalized}="
383
+ return unless respond_to?(setter)
384
+
385
+ setter if self.class.attribute?(normalized) || user_defined_method?(setter)
386
+ end
387
+
358
388
  # Merge caller-provided attributes with configured defaults.
359
389
  # Only keys the caller explicitly passed are treated as overrides;
360
390
  # method-signature defaults ([], {}, false) are NOT present unless the caller wrote them.
@@ -403,7 +433,7 @@ module ClaudeAgentSDK
403
433
  # reports the typo exactly as the developer wrote it.
404
434
  def option_key(name)
405
435
  normalized = normalize_name(name)
406
- respond_to?(:"#{normalized}=") ? normalized.to_sym : name
436
+ option_setter(normalized) ? normalized.to_sym : name
407
437
  end
408
438
  end
409
439
  end
@@ -34,20 +34,21 @@ module ClaudeAgentSDK
34
34
  @rules = value&.map { |rule| rule.is_a?(Hash) ? PermissionRuleValue.new(rule) : rule }
35
35
  end
36
36
 
37
+ # The wire form. The CLI validates the updatedPermissions of a
38
+ # can_use_tool reply as one unit and drops the whole array when a single
39
+ # entry does not fit its schema, so two things it rejects are never
40
+ # written: a rule without content has no ruleContent key (the schema
41
+ # wants a String or no key, not null), and an update that has a type but
42
+ # no destination goes to 'session' — the narrowest one: it lasts for this
43
+ # run and writes no settings file (the default Python PR #1330 proposes).
37
44
  def to_h
38
45
  result = { type: @type }
39
- result[:destination] = @destination if @destination
46
+ destination = @destination || ('session' unless @type.nil?)
47
+ result[:destination] = destination if destination
40
48
 
41
49
  case @type
42
50
  when 'addRules', 'replaceRules', 'removeRules'
43
- if @rules
44
- result[:rules] = @rules.map do |rule|
45
- {
46
- toolName: rule.tool_name,
47
- ruleContent: rule.rule_content
48
- }
49
- end
50
- end
51
+ result[:rules] = @rules.map { |rule| rule_to_h(rule) } if @rules
51
52
  result[:behavior] = @behavior if @behavior
52
53
  when 'setMode'
53
54
  result[:mode] = @mode if @mode
@@ -57,6 +58,14 @@ module ClaudeAgentSDK
57
58
 
58
59
  result
59
60
  end
61
+
62
+ private
63
+
64
+ def rule_to_h(rule)
65
+ wire_rule = { toolName: rule.tool_name }
66
+ wire_rule[:ruleContent] = rule.rule_content unless rule.rule_content.nil?
67
+ wire_rule
68
+ end
60
69
  end
61
70
 
62
71
  # Tool permission context delivered to `can_use_tool` callbacks.
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module ClaudeAgentSDK
4
- VERSION = '1.1.0'
4
+ VERSION = '1.2.0'
5
5
  end
@@ -79,7 +79,10 @@ module ClaudeAgentSDK # rubocop:disable Metrics/ModuleLength -- the public entry
79
79
  end
80
80
 
81
81
  # Internal: normalize hook lists for the control protocol. An absent or
82
- # disabled event must not become an empty registration in initialize.
82
+ # disabled event must not become an empty registration in initialize. An
83
+ # event written as a String under one key and as a Symbol under another
84
+ # ('PreToolUse' and :PreToolUse) is one event: its matcher lists are
85
+ # joined in the order they were written, never replaced.
83
86
  # @api private
84
87
  def self.convert_hooks_to_internal_format(hooks)
85
88
  return nil unless hooks
@@ -94,7 +97,7 @@ module ClaudeAgentSDK # rubocop:disable Metrics/ModuleLength -- the public entry
94
97
  config[:timeout] = matcher.timeout if matcher.timeout
95
98
  entries << config
96
99
  end
97
- internal_hooks[event.to_s] = entries unless entries.empty?
100
+ (internal_hooks[event.to_s] ||= []).concat(entries) unless entries.empty?
98
101
  end
99
102
  internal_hooks.empty? ? nil : internal_hooks
100
103
  end
@@ -139,7 +142,8 @@ module ClaudeAgentSDK # rubocop:disable Metrics/ModuleLength -- the public entry
139
142
  eds = system_prompt.exclude_dynamic_sections
140
143
  return eds if [true, false].include?(eds)
141
144
  elsif system_prompt.is_a?(Hash)
142
- type = system_prompt[:type] || system_prompt['type']
145
+ # The tag may be a Symbol (type: :preset), as CommandBuilder reads it.
146
+ type = (system_prompt[:type] || system_prompt['type']).to_s
143
147
  if type == 'preset'
144
148
  eds = system_prompt.fetch(:exclude_dynamic_sections) { system_prompt['exclude_dynamic_sections'] }
145
149
  return eds if [true, false].include?(eds)
@@ -160,7 +164,7 @@ module ClaudeAgentSDK # rubocop:disable Metrics/ModuleLength -- the public entry
160
164
  snapshot = system_prompt.snapshot
161
165
  return snapshot if [true, false].include?(snapshot)
162
166
  when Hash
163
- type = system_prompt[:type] || system_prompt['type']
167
+ type = (system_prompt[:type] || system_prompt['type']).to_s
164
168
  if %w[preset custom].include?(type)
165
169
  snapshot = system_prompt.fetch(:snapshot) { system_prompt['snapshot'] }
166
170
  return snapshot if [true, false].include?(snapshot)
@@ -173,14 +177,39 @@ module ClaudeAgentSDK # rubocop:disable Metrics/ModuleLength -- the public entry
173
177
  # Each observer is invoked through FiberBoundary so that user code runs
174
178
  # on a plain thread (no Fiber scheduler) even when called from inside
175
179
  # the SDK's Async reactor — or in place when scheduling is :inline.
180
+ #
181
+ # What the observer raises, and what its callback_wrapper raises, is
182
+ # contained where it happens: inside the hop, on the execution context
183
+ # the observer runs on. The rescue must not sit around the hop, because
184
+ # the calling fiber WAITS there, and an exception raised into a waiting
185
+ # fiber is not the observer's — a caller's `task.with_timeout` /
186
+ # `Timeout.timeout` deadline is delivered exactly that way. Rescued out
187
+ # here, an expired deadline was swallowed as if the observer had failed,
188
+ # and the turn carried on. So the wrapper is composed inside the rescued
189
+ # body rather than handed to FiberBoundary.invoke, which would run it
190
+ # outside that rescue.
191
+ #
192
+ # Known residue with `scheduling: :inline` (and with no scheduler at all):
193
+ # the observer runs on the calling fiber itself, so a deadline that lands
194
+ # while the observer is suspended is raised inside the observer's own
195
+ # frames, cannot be told from the observer's own timeout, and is still
196
+ # swallowed here.
176
197
  # @api private
177
198
  def self.notify_observers(observers, method, *args, scheduling: :thread, wrapper: nil)
178
199
  observers.each do |obs|
179
- FiberBoundary.invoke(scheduling: scheduling, wrapper: wrapper) { obs.send(method, *args) }
180
- rescue StandardError, ScriptError
181
- # ScriptError too: NotImplementedError < ScriptError (not
182
- # StandardError), and a stubbed observer must never mask the original
183
- # error being notified or abort connect/teardown cleanup.
200
+ FiberBoundary.invoke(scheduling: scheduling) do
201
+ invocation = proc { obs.send(method, *args) }
202
+ wrapper ? wrapper.call(invocation) : invocation.call
203
+ rescue StandardError, ScriptError
204
+ # ScriptError too: NotImplementedError < ScriptError (not
205
+ # StandardError), and a stubbed observer must never mask the original
206
+ # error being notified or abort connect/teardown cleanup.
207
+ nil
208
+ end
209
+ rescue ScriptError
210
+ # A ScriptError raised around the body rather than in it (the hop's own
211
+ # plumbing). No deadline is a ScriptError, so nothing a caller injects
212
+ # into the waiting fiber is caught here.
184
213
  nil
185
214
  end
186
215
  end
@@ -703,7 +732,14 @@ module ClaudeAgentSDK # rubocop:disable Metrics/ModuleLength -- the public entry
703
732
  raise ArgumentError, 'transport must respond to #connect (see ClaudeAgentSDK::Transport)'
704
733
  end
705
734
 
706
- Async(&FiberBoundary.capture_otel_context do # rubocop:disable Metrics/BlockLength -- the reactor task body of query()
735
+ # finished: false tells Async that a waiter handles this task's failure
736
+ # (the .wait at the end re-raises it), as Kernel#Sync does for its own
737
+ # task. Without it a task that fails before anyone waits for it — always
738
+ # the case outside a reactor, where Async runs the whole task before it
739
+ # returns — is also logged as "Task may have ended with unhandled
740
+ # exception", message and backtrace included, although the caller gets
741
+ # the same error raised and may well rescue it.
742
+ Async(finished: false, &FiberBoundary.capture_otel_context do # rubocop:disable Metrics/BlockLength -- the reactor task body of query()
707
743
  materialized = nil
708
744
  query_handler = nil
709
745
  begin
@@ -903,16 +939,13 @@ module ClaudeAgentSDK # rubocop:disable Metrics/ModuleLength -- the public entry
903
939
  # The Client class always uses streaming mode for bidirectional communication.
904
940
  #
905
941
  # @example Basic usage
906
- # Async do
907
- # client = ClaudeAgentSDK::Client.new
908
- # client.connect # No arguments needed - automatically uses streaming mode
909
- #
942
+ # # Client.open connects, yields the client and always disconnects,
943
+ # # also when the block raises.
944
+ # ClaudeAgentSDK::Client.open do |client|
910
945
  # client.query("What is the capital of France?")
911
946
  # client.receive_response do |msg|
912
947
  # puts msg if msg.is_a?(ClaudeAgentSDK::AssistantMessage)
913
948
  # end
914
- #
915
- # client.disconnect
916
949
  # end
917
950
  #
918
951
  # @example With hooks
@@ -1043,17 +1076,22 @@ module ClaudeAgentSDK # rubocop:disable Metrics/ModuleLength -- the public entry
1043
1076
  # (The enumerator branch streams in the background and cannot raise
1044
1077
  # out of connect.) No on_close follows for pre-handshake failures
1045
1078
  # (disconnect gates it on @connected): the session never opened.
1046
- notify_error(e) if e.is_a?(StandardError) && !@connected
1047
- # Tear down the partial connect, but never let a cleanup failure (e.g. a
1048
- # custom transport whose #close raises) mask the original connect error.
1049
- # Rescue Exception (not StandardError) so reactor cancellation
1050
- # (Async::Stop < Exception) after materialize_resume set @materialized
1051
- # still runs disconnect -> @materialized.cleanup, never leaking the temp
1052
- # CLAUDE_CONFIG_DIR that holds the redacted .credentials.json copy.
1053
1079
  begin
1054
- disconnect
1055
- rescue StandardError => cleanup_error
1056
- warn "Claude SDK: cleanup after failed connect raised: #{cleanup_error.message}"
1080
+ notify_error(e) if e.is_a?(StandardError) && !@connected
1081
+ ensure
1082
+ # Tear down the partial connect, but never let a cleanup failure (e.g. a
1083
+ # custom transport whose #close raises) mask the original connect error.
1084
+ # Rescue Exception (not StandardError) so reactor cancellation
1085
+ # (Async::Stop < Exception) after materialize_resume set @materialized
1086
+ # still runs disconnect -> @materialized.cleanup, never leaking the temp
1087
+ # CLAUDE_CONFIG_DIR that holds the redacted .credentials.json copy.
1088
+ # In an ensure: an exception raised into this fiber while it waits
1089
+ # for the on_error observer (a caller's deadline) must not skip it.
1090
+ begin
1091
+ disconnect
1092
+ rescue StandardError => cleanup_error
1093
+ warn "Claude SDK: cleanup after failed connect raised: #{cleanup_error.message}"
1094
+ end
1057
1095
  end
1058
1096
  raise
1059
1097
  end
@@ -1314,11 +1352,11 @@ module ClaudeAgentSDK # rubocop:disable Metrics/ModuleLength -- the public entry
1314
1352
  # holds, in every scheduling mode, for a streaming-input enumerator that
1315
1353
  # calls disconnect: it is iterated on the reactor inside a task the close
1316
1354
  # stops, so it unwinds with Async::Stop once the teardown has completed.
1355
+ #
1356
+ # Interrupted while an observer's on_close runs — the caller's deadline
1357
+ # expires, or its task is stopped — disconnect still completes the
1358
+ # teardown, then lets the interruption propagate.
1317
1359
  def disconnect
1318
- if @connected
1319
- ClaudeAgentSDK.notify_observers(@resolved_observers || [], :on_close,
1320
- scheduling: @callback_scheduling, wrapper: @callback_wrapper)
1321
- end
1322
1360
  # Tear down whatever exists — robust to a partial/failed connect, where
1323
1361
  # @connected is still false but a transport and/or materialized temp dir
1324
1362
  # were already created. #close on the query handler also closes the
@@ -1335,26 +1373,36 @@ module ClaudeAgentSDK # rubocop:disable Metrics/ModuleLength -- the public entry
1335
1373
  # and the materialized-dir decision at the bottom needs to ask it.
1336
1374
  query_handler = @query_handler
1337
1375
  begin
1338
- @query_handler&.close
1376
+ # Notified inside the begin: an exception raised into this fiber while
1377
+ # it waits for an observer (a caller's deadline) propagates, and must
1378
+ # not skip the teardown below.
1379
+ if @connected
1380
+ ClaudeAgentSDK.notify_observers(@resolved_observers || [], :on_close,
1381
+ scheduling: @callback_scheduling, wrapper: @callback_wrapper)
1382
+ end
1339
1383
  ensure
1340
- @query_handler = nil
1341
1384
  begin
1342
- @transport&.close
1385
+ @query_handler&.close
1343
1386
  ensure
1344
- @transport = nil
1345
- @connected = false
1346
- # Remove the materialized resume temp dir AFTER the subprocess
1347
- # exited — unless the mirror dropped batches: the store copy is then
1348
- # incomplete and the temp dir holds the only copy of the dropped
1349
- # turns, so it is preserved (scrubbed of credentials) with a warning
1350
- # instead of deleted.
1351
- if @materialized
1352
- if query_handler&.mirror_batches_dropped?
1353
- @materialized.preserve_transcripts
1354
- else
1355
- @materialized.cleanup
1387
+ @query_handler = nil
1388
+ begin
1389
+ @transport&.close
1390
+ ensure
1391
+ @transport = nil
1392
+ @connected = false
1393
+ # Remove the materialized resume temp dir AFTER the subprocess
1394
+ # exited — unless the mirror dropped batches: the store copy is then
1395
+ # incomplete and the temp dir holds the only copy of the dropped
1396
+ # turns, so it is preserved (scrubbed of credentials) with a warning
1397
+ # instead of deleted.
1398
+ if @materialized
1399
+ if query_handler&.mirror_batches_dropped?
1400
+ @materialized.preserve_transcripts
1401
+ else
1402
+ @materialized.cleanup
1403
+ end
1404
+ @materialized = nil
1356
1405
  end
1357
- @materialized = nil
1358
1406
  end
1359
1407
  end
1360
1408
  end
@@ -22,6 +22,12 @@ ClaudeAgentSDK.configure do |config|
22
22
  # whatever the process cwd. To run a binary from somewhere else instead:
23
23
  # cli_path: '/usr/local/bin/claude',
24
24
 
25
+ # Multi-user apps: turn off the CLI's auto-memory — kept per project
26
+ # directory, read by every session and written by claude_code-preset
27
+ # sessions, so one user's "remember ..." reaches all others (the value
28
+ # must stay '1'; '0' forces it on). See docs/rails.md ("Per-user isolation").
29
+ # env: { 'CLAUDE_CODE_DISABLE_AUTO_MEMORY' => '1' },
30
+
25
31
  # OpenTelemetry tracing (Langfuse, Honeycomb, ...). A factory lambda gives
26
32
  # every query/session its own observer — safe under Puma and job threads.
27
33
  # observers: [-> { ClaudeAgentSDK::Instrumentation::OTelObserver.new }],
@@ -47,8 +47,9 @@ module ClaudeAgentSDK
47
47
  | CwdChangedHookSpecificOutput
48
48
  | FileChangedHookSpecificOutput
49
49
 
50
- # What a hook callback returns: a Hash (snake_case or camelCase keys), a
51
- # typed output, or nil (treated as {}).
50
+ # What a hook callback returns: a Hash (Symbol or String keys, snake_case
51
+ # or camelCase, also inside hook_specific_output), a typed output, or nil
52
+ # (treated as {}).
52
53
  type hook_output = Hash[Symbol | String, untyped] | SyncHookJSONOutput | AsyncHookJSONOutput | nil
53
54
 
54
55
  # A hook callback (HookMatcher#hooks entries). tool_use_id is nil for
@@ -520,7 +521,9 @@ module ClaudeAgentSDK
520
521
 
521
522
  attr_accessor reason: String?
522
523
 
523
- # A typed output, or the equivalent Hash (camelCase wire keys, sent as-is).
524
+ # A typed output, or the equivalent Hash (snake_case or camelCase keys).
525
+ # #to_h returns a Hash as written; the SDK puts its keys in wire spelling
526
+ # when it sends the output.
524
527
  attr_accessor hook_specific_output: (hook_specific_output | Hash[Symbol | String, untyped])?
525
528
 
526
529
  def to_h: () -> Hash[Symbol, untyped]
@@ -7,6 +7,16 @@ module ClaudeAgentSDK
7
7
  # the Hash form (String or camelCase keys, e.g. `klass.new(value.to_h)`)
8
8
  # is accepted too. An unknown key raises ArgumentError. Discriminators
9
9
  # (`type`) are fixed by the class and read-only.
10
+ #
11
+ # The keyword overload documents the accepted keys and their types; no
12
+ # type checker enforces it. `klass.new(typo: 1)` is also a Symbol-keyed
13
+ # Hash, which the second overload accepts whatever its keys and values, so
14
+ # Steep reports neither a misspelled keyword nor a wrong value type, and
15
+ # the runtime checker behind `rake rbs:test` never reports a constructor
16
+ # mismatch (it only checks the attribute setters the constructor calls).
17
+ # The enforcement is the ArgumentError the constructor raises at runtime
18
+ # for an unknown key. This holds for every constructor in sig/ that has
19
+ # both overloads.
10
20
 
11
21
  # Adaptive thinking: the model decides when and how much to think.
12
22
  class ThinkingConfigAdaptive < Type
@@ -81,15 +91,15 @@ module ClaudeAgentSDK
81
91
 
82
92
  # A local plugin directory (ClaudeAgentOptions#plugins entries).
83
93
  class SdkPluginConfig < Type
84
- def initialize: (?path: String?) -> void
94
+ def initialize: (?path: (String | Pathname)?) -> void
85
95
  | (Hash[Symbol | String, untyped]? attributes) -> void
86
96
 
87
- attr_accessor path: String?
97
+ attr_accessor path: (String | Pathname)?
88
98
 
89
99
  # "local".
90
100
  attr_reader type: String
91
101
 
92
- def to_h: () -> { type: String, path: String? }
102
+ def to_h: () -> { type: String, path: (String | Pathname)? }
93
103
  end
94
104
 
95
105
  class SandboxNetworkConfig < Type
@@ -138,6 +148,13 @@ module ClaudeAgentSDK
138
148
 
139
149
  # Sandbox settings for isolated command execution
140
150
  # (ClaudeAgentOptions#sandbox).
151
+ #
152
+ # network / filesystem take the typed config or a Hash. A Hash may spell
153
+ # the fields of the typed class in snake_case or in the CLI's camelCase,
154
+ # with Symbol or String keys; other keys are sent as written. A snake_case
155
+ # key is sent under the CLI's name only when its value has the shape the
156
+ # CLI accepts for it; otherwise it is sent as written, which the CLI
157
+ # ignores.
141
158
  class SandboxSettings < Type
142
159
  def initialize: (?enabled: bool?, ?fail_if_unavailable: bool?, ?auto_allow_bash_if_sandboxed: bool?, ?excluded_commands: Array[String]?, ?allow_unsandboxed_commands: bool?, ?network: (SandboxNetworkConfig | Hash[Symbol | String, untyped])?, ?filesystem: (SandboxFilesystemConfig | Hash[Symbol | String, untyped])?, ?ignore_violations: Hash[Symbol | String, Array[String]]?, ?enable_weaker_nested_sandbox: bool?, ?enable_weaker_network_isolation: bool?, ?ripgrep: Hash[Symbol | String, untyped]?) -> void
143
160
  | (Hash[Symbol | String, untyped]? attributes) -> void
@@ -181,15 +198,15 @@ module ClaudeAgentSDK
181
198
 
182
199
  # A system prompt loaded from a file.
183
200
  class SystemPromptFile < Type
184
- def initialize: (?path: String?) -> void
201
+ def initialize: (?path: (String | Pathname)?) -> void
185
202
  | (Hash[Symbol | String, untyped]? attributes) -> void
186
203
 
187
- attr_accessor path: String?
204
+ attr_accessor path: (String | Pathname)?
188
205
 
189
206
  # "file".
190
207
  attr_reader type: String
191
208
 
192
- def to_h: () -> { type: String, path: String? }
209
+ def to_h: () -> { type: String, path: (String | Pathname)? }
193
210
  end
194
211
 
195
212
  # The Claude Code preset system prompt, optionally extended.
@@ -48,7 +48,7 @@ module ClaudeAgentSDK
48
48
  ?permission_prompt_tool_name: String?,
49
49
  ?cwd: (String | Pathname)?,
50
50
  ?cli_path: (String | Pathname)?,
51
- ?settings: (String | Hash[Symbol | String, untyped])?,
51
+ ?settings: (String | Pathname | Hash[Symbol | String, untyped])?,
52
52
  ?add_dirs: Array[String | Pathname]?,
53
53
  ?env: Hash[String | Symbol, String?]?,
54
54
  ?extra_args: Hash[String | Symbol, untyped]?,
@@ -68,7 +68,7 @@ module ClaudeAgentSDK
68
68
  ?plugins: Array[SdkPluginConfig | Hash[Symbol | String, untyped]]?,
69
69
  ?debug_stderr: (_Puts | String)?,
70
70
  ?betas: Array[String]?,
71
- ?tools: (Array[String] | ToolsPreset | Hash[Symbol | String, untyped])?,
71
+ ?tools: (Array[String] | ToolsPreset | Hash[Symbol | String, untyped] | String)?,
72
72
  ?sandbox: (SandboxSettings | Hash[Symbol | String, untyped] | bool)?,
73
73
  ?thinking: thinking_config?,
74
74
  ?effort: (String | Symbol | Integer)?,
@@ -133,8 +133,9 @@ module ClaudeAgentSDK
133
133
 
134
134
  attr_accessor cli_path: (String | Pathname)?
135
135
 
136
- # A settings JSON String, a path to a settings file, or a Hash.
137
- attr_accessor settings: (String | Hash[Symbol | String, untyped])?
136
+ # A settings JSON String, a path to a settings file (a String or a
137
+ # Pathname), or a Hash.
138
+ attr_accessor settings: (String | Pathname | Hash[Symbol | String, untyped])?
138
139
 
139
140
  attr_accessor add_dirs: Array[String | Pathname]?
140
141
 
@@ -185,10 +186,13 @@ module ClaudeAgentSDK
185
186
  # Entries of SDK_BETAS.
186
187
  attr_accessor betas: Array[String]?
187
188
 
188
- # The base tool set: tool names, or a preset.
189
- attr_accessor tools: (Array[String] | ToolsPreset | Hash[Symbol | String, untyped])?
189
+ # The base tool set: tool names (an Array, or the CLI's comma-separated
190
+ # String such as "Read,Grep"), or a preset.
191
+ attr_accessor tools: (Array[String] | ToolsPreset | Hash[Symbol | String, untyped] | String)?
190
192
 
191
- # true / false are forwarded as-is (a bare toggle).
193
+ # true / false are forwarded as-is (a bare toggle). A Hash may spell the
194
+ # fields of SandboxSettings, and of its network / filesystem, in
195
+ # snake_case or in the CLI's camelCase (see SandboxSettings).
192
196
  attr_accessor sandbox: (SandboxSettings | Hash[Symbol | String, untyped] | bool)?
193
197
 
194
198
  attr_accessor thinking: thinking_config?
@@ -41,7 +41,8 @@ module ClaudeAgentSDK
41
41
 
42
42
  attr_accessor directories: Array[String]?
43
43
 
44
- # One of PERMISSION_UPDATE_DESTINATIONS.
44
+ # One of PERMISSION_UPDATE_DESTINATIONS. When nil, #to_h sends "session"
45
+ # (unless type is nil too).
45
46
  attr_accessor destination: String?
46
47
 
47
48
  attr_reader rules: Array[PermissionRuleValue]?
@@ -49,7 +50,8 @@ module ClaudeAgentSDK
49
50
  # Hash entries (camelCase or snake_case keys) become PermissionRuleValue.
50
51
  def rules=: (Array[PermissionRuleValue | Hash[Symbol | String, untyped]]? value) -> Array[PermissionRuleValue | Hash[Symbol | String, untyped]]?
51
52
 
52
- # The wire form (camelCase keys).
53
+ # The wire form (camelCase keys). A rule without content has no
54
+ # ruleContent key.
53
55
  def to_h: () -> Hash[Symbol, untyped]
54
56
  end
55
57
 
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: claude-agent-sdk
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.1.0
4
+ version: 1.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - ya-luotao
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2026-09-30 00:00:00.000000000 Z
11
+ date: 2026-10-03 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: async
@@ -54,14 +54,14 @@ dependencies:
54
54
  name: bundler
55
55
  requirement: !ruby/object:Gem::Requirement
56
56
  requirements:
57
- - - "~>"
57
+ - - ">="
58
58
  - !ruby/object:Gem::Version
59
59
  version: '2.0'
60
60
  type: :development
61
61
  prerelease: false
62
62
  version_requirements: !ruby/object:Gem::Requirement
63
63
  requirements:
64
- - - "~>"
64
+ - - ">="
65
65
  - !ruby/object:Gem::Version
66
66
  version: '2.0'
67
67
  - !ruby/object:Gem::Dependency
@@ -118,6 +118,7 @@ executables: []
118
118
  extensions: []
119
119
  extra_rdoc_files: []
120
120
  files:
121
+ - ".yardopts"
121
122
  - CHANGELOG.md
122
123
  - LICENSE
123
124
  - README.md
@@ -129,6 +130,7 @@ files:
129
130
  - docs/hooks-and-permissions.md
130
131
  - docs/mcp-servers.md
131
132
  - docs/observability.md
133
+ - docs/options.md
132
134
  - docs/rails.md
133
135
  - docs/sessions.md
134
136
  - docs/subagents.md