Repository navigation
Releases: rubycatco/claude-agent-sdk-ruby
Release list
v1.4.3
Syncs with Python SDK 0.2.174 (previously 0.2.162). Python #1331 and #1332 were already in 1.2.0, and SandboxSettings#ignore_violations already took the command-pattern Hash that Python #1380 fixed its type to; every other release in the range only bumps the CLI Python bundles.
Fixed
get_session_messagesandget_subagent_messages(disk andsession_store:) no longer drop the tool calls of a parallel batch (Python SDK #1422). The conversation goes on from the tool result written last. When that result answers an earlier call of the batch than the last one, thetool_useentries written after that call were off the path the readers walk, so 1.2.0's fix (#157) never saw them: the readers returned neither those calls nor their results. Of the parallel batches in one developer's local transcripts, about 7% have this shape. The readers now put back every entry of an API message on the conversation, right after its last entry there (in timestamp order), then the results as before. A result that the CLI parented on a hook attachment instead of its call's entry (itssourceToolAssistantUUIDstill names the call) is put back as well, when the same agent wrote both. The guards from #157 hold: the chain's own result wins, at most one result comes back per call, and the result of a branch a rewind dropped stays out. That last guard was also too wide on real transcripts, and missed results #157 meant to return: going on from a result no longer counts as a dropped branch when what goes on is a later call of the same batch, another result of the batch (the CLI sometimes parents one under an earlier result's hook attachment), or a meta entry (the[Image: ...]note the CLI writes under a screenshot result). A branch that goes on through a meta entry or through another result of the batch is still recognized. Affected sessions return more messages than before, sooffset:/limit:pages over them shift. Python fixes the main-session readers only; here both readers are fixed.
Changed
docs/options.md:include_hook_eventsdoes not turn off theSessionStartandSetuphook events, which the CLI sends whether or not it is set (Python SDK #1336).CLIInstaller::PINNED_CLI_VERSIONmoves from 2.1.295 to 2.1.311, following the CLI the Python SDK bundles.CLIInstaller.install_pinnedinstalls it. See the Claude Code changelog.
What's changed
- #181 Sync with Python SDK 0.2.174 — @ya-luotao
- #180 Bump PINNED_CLI_VERSION to 2.1.311 — @github-actions
Full Changelog: v1.4.2...v1.4.3
gem install claude-agent-sdk -v 1.4.3
v1.4.2
Fixed
bin/rails claude_agent_sdk:install_cli(andCLIInstaller.installwheneverEncoding.default_internalis set) no longer fails withEncoding::UndefinedConversionError(issue #177). The download was opened withFile::BINARY, which is 0 outside Windows, so the file was in text mode; Rails setsdefault_internalto UTF-8, and each write then transcoded the binary chunks and raised on the first byte that is not valid UTF-8. The file is now opened withbinmode: true.
What's changed
- #179 CLIInstaller: write the downloaded CLI in binary mode on every platform — @ya-luotao
Full Changelog: v1.4.1...v1.4.2
gem install claude-agent-sdk -v 1.4.2
v1.4.1
Changed
process_group: true: a SIGKILL the SDK sends the CLI also goes to the CLI's process group. A wedged CLI that the SDK had to kill could not stop its own children, so one still in its group that ignores stdin EOF (a stdio MCP server, for instance) kept running after the session; beforeprocess_groupit would at least have got the terminal's Ctrl-C. The group SIGKILL goes out just before the SIGKILL of the CLI itself, whenever the SDK sends one (a normal teardown after its grace periods and SIGTERM, a cancelled teardown after its short grace, or a CLI that closed stdout and hung), and adds no delay. SIGTERM, including the one sent at interpreter exit, still goes to the CLI process only, so the CLI's own cleanup of its children still runs; a CLI that exits on SIGTERM or on its own gets no group signal, and processes that left its group are not reached. Only sessions withprocess_group: trueon Unix are affected. This narrows what 1.4.0 documented ("the SDK still signals the CLI process, not its group"). Seedocs/options.md.
What's changed
- #178 process_group: send the SDK's SIGKILL of the CLI to its process group too — @ya-luotao
Full Changelog: v1.4.0...v1.4.1
gem install claude-agent-sdk -v 1.4.1
v1.4.0
Added
ClaudeAgentOptions#process_groupstarts the CLI process in a process group of its own (issue #175). In a terminal, Ctrl-C signals the whole foreground process group, so the CLI used to get the SIGINT along with your process and end the turn on its own before your code could decide what to do. Withprocess_group: truethe terminal's Ctrl-C no longer reaches the CLI, and your process owns it: callClient#interruptordisconnectfrom ordinary code (not from inside the signal trap, where Ruby raisesThreadError). Off by default, so nothing changes unless you set it. Sends nothing to the CLI; usespgroup: trueon Unix andnew_pgroup: trueon Windows, where it is untested. Teardown is unchanged: the SDK still signals the CLI process, not its group. Ruby only: the Python SDK's equivalent (PR #617) is unmerged as of 2026-10-25.docs/options.mdcovers the trap pattern, when the CLI stops, and the background-group caveat for tools that read the terminal.
Changed
- README: the Claude Code overview link (it led to a "Page not found" page) and the built-in tools link now point at
code.claude.com. "Rails in a minute" is a section of its own after the Quick Start, and its job turns off auto-memory (CLAUDE_CODE_DISABLE_AUTO_MEMORY) as the Rails guide's does. The hooks example defines the two callbacks it passes, the custom-tool example runs a prompt, and the Examples table listssubagent_status_example.rb. What a session without credentials, or with a rejected key, returns is now described in a new "Authentication Failures" section ofdocs/errors.md.
Usage
interrupts = Thread::Queue.new
previous = trap('INT') { interrupts << :int } # nothing else in here: no SDK calls
begin
options = ClaudeAgentSDK::ClaudeAgentOptions.new(process_group: true)
ClaudeAgentSDK::Client.open(options: options) do |client|
watcher = Thread.new do
client.interrupt if interrupts.pop # on this thread, outside the trap
end
client.query('Refactor the parser')
client.receive_response { |message| puts message }
ensure
interrupts << nil # ends the watcher; the queue stays open for the trap
watcher&.join
end
ensure
trap('INT', previous) # Ctrl-C behaves as before once the session is over
endWhat's changed
- #176 Add ClaudeAgentOptions#process_group: start the CLI in its own process group — @ya-luotao
Full Changelog: v1.3.0...v1.4.0
gem install claude-agent-sdk -v 1.4.0
v1.3.0
Added
- Every hook input carries
raw_input, the payload exactly as the CLI sent it. It used to be onUnknownHookInputonly; a typed input copied the fields its class names and dropped the rest, so a field a newer CLI added was out of reach until the SDK learned it. Hook inputs are now built from the whole payload (every field the class declares, read from the same key as before).#inspectshows it on anUnknownHookInputonly: a typed input already prints those fields as attributes. - Hook input fields the CLI already sends (Python SDK #1331, plus the schemas of Claude Code 2.1.295):
prompt_idandefforton every hook input;duration_msonPostToolUseHookInputandPostToolUseFailureHookInput;session_titleandsourceonUserPromptSubmitHookInput;session_title,seconds_since_last_response,prompt_cache_likely_expired,context_tokensandestimated_cache_write_usdonSessionStartHookInput;parent_file_pathonInstructionsLoadedHookInput. - Typed inputs for the hook events
PostToolBatch,UserPromptExpansion,PreModelSwitch,PostModelSwitch,DirectoryAddedandMessageDisplay(PostToolBatchHookInput, ...), which used to arrive asUnknownHookInput.HOOK_EVENTSlists them, so it now has the CLI's 33 events. - Hook-specific outputs from Python SDK #1331:
PostToolBatchHookSpecificOutput,UserPromptExpansionHookSpecificOutput,StopHookSpecificOutput,SubagentStopHookSpecificOutput,PreModelSwitchHookSpecificOutput,PostModelSwitchHookSpecificOutput,ElicitationHookSpecificOutput,ElicitationResultHookSpecificOutput,WorktreeCreateHookSpecificOutputandMessageDisplayHookSpecificOutput;session_titleandsuppress_original_promptonUserPromptSubmitHookSpecificOutput;initial_user_message,session_title,watch_pathsandreload_skillsonSessionStartHookSpecificOutput. A Hash return value could already send these keys; the typed classes rejected them withArgumentError.docs/hooks-and-permissions.mdnow lists every output class and its fields. InitMessage#capabilities,#plugin_errors,#terminal_slash_commandsand#fast_mode_disabled_reason, the init fields of Python SDK #1332 (SystemInitData) thatInitMessagedid not read. Testcapabilities(for example for"interrupt_receipt_v1") instead of comparingclaude_code_version.
Changed
- The repository moves to the rubycatco GitHub organization:
github.com/rubycatco/claude-agent-sdk-ruby. The oldya-luotao/...URL redirects, and the gem on rubygems.org is unaffected; gem metadata, docs, templates and issue links now point at the new home. PermissionDeniedHookSpecificOutput#retrydefaults toniland is then left out of the output, as in Python; it used to default tofalseand always sendretry: false, which the CLI reads the same way. Code that reads#retryback from a fresh instance getsnil.- The
*HookSpecificOutputclasses are declared from one table of wire keys (HookOutputKeys::HOOK_SPECIFIC) instead of each writing its own#to_h. Each class still sends the same keys, except that a field set tofalseis now sent (onlynilis left out); before, most fields also droppedfalse. CLIInstaller::PINNED_CLI_VERSIONmoves from 2.1.288 to 2.1.295, following the CLI the Python SDK bundles.CLIInstaller.install_pinnedinstalls it. See the Claude Code changelog.
Usage
hook = lambda do |input, _tool_use_id, _context|
input.prompt_id # new on every hook input
input.raw_input[:scratchpad_dir] # a field this SDK version has no reader for
ClaudeAgentSDK::SyncHookJSONOutput.new(
hook_specific_output: ClaudeAgentSDK::UserPromptSubmitHookSpecificOutput.new(session_title: 'Staging rollout')
)
end
options = ClaudeAgentSDK::ClaudeAgentOptions.new(
hooks: { 'UserPromptSubmit' => [ClaudeAgentSDK::HookMatcher.new(hooks: [hook])] }
)What's changed
- #166 Bump PINNED_CLI_VERSION to 2.1.289 — @github-actions
- #170 Bump PINNED_CLI_VERSION to 2.1.291 — @github-actions
- #171 Bump PINNED_CLI_VERSION to 2.1.293 — @github-actions
- #172 Bump PINNED_CLI_VERSION to 2.1.295 — @github-actions
- #173 Move repository references to the rubycatco organization — @ya-luotao
- #174 Follow Python SDK #1331/#1332: hook types, raw hook input, init fields — @ya-luotao
Full Changelog: v1.2.1...v1.3.0
gem install claude-agent-sdk -v 1.3.0
v1.2.1
An internal release: three refactors that move duplicated or scattered logic into one private module each. No public API, signature (sig/) or documented behavior changes; one cleanup difference is noted below.
Changed
ClaudeAgentSDK.queryandClientshare one implementation of session setup, prompt writes, message delivery and teardown (SessionAssembly,@api private); a session's observers,callback_schedulingandcallback_wrappertravel in one object (Dispatch,@api private) instead of being passed at every call site. The documented differences between the two entry points are preserved and now pinned by specs that run both side by side. One cleanup difference: when aClientis reconnected while its previousdisconnectis still removing the materialized directory of a store-backed resume, the finishingdisconnectno longer clears the new session, so the new session's resume directory is removed by its owndisconnect(it used to be left behind). (#169)- The decision of when
query()may close stdin lives in one private class,Query::RunLifecycle;Queryfeeds it events and supplies the timer. Its specs go through that interface instead ofQuery's instance variables. (#168) - Hash-form option interpretation (
system_prompt,thinking,tools,output_format,task_budget,plugins,sandbox,mcp_servers,agents) lives in one private module,OptionForms; every existing per-option spelling and key policy is preserved as it was. (#167)
What's changed
- #167 Move Hash-form option interpretation into one private module (OptionForms) — @ya-luotao
- #168 Extract the run-end decision into Query::RunLifecycle — @ya-luotao
- #169 Share one session assembly between query() and Client — @ya-luotao
Full Changelog: v1.2.0...v1.2.1
gem install claude-agent-sdk -v 1.2.1
v1.2.0
Fixes from a review of the whole SDK, a reference for every option (docs/options.md), and Claude Code 2.1.288 as the pinned CLI. Settings the SDK used to drop without an error now take effect — hook outputs and sandbox: Hashes in Ruby spelling, a tools: String, option Hashes with a Symbol type — so read the entries in bold before upgrading: they say what changes for code that relied on the old behavior.
Added
docs/options.md: a reference for everyClaudeAgentOptionsattribute — its type, its default, and the CLI flag,initializefield or environment variable it becomes — plus what an option reads when it is left out, passed asnilor given a configured default, and the environment variables the SDK reads. It documents options that had no entry anywhere before (extra_args,include_partial_messages,include_hook_events,task_budget,strict_mcp_config) and theCLAUDE_CODE_PRINT_BG_WAIT_CEILING_MSceiling from 1.1.0. A spec checks every row against the code.docs/client.mddocuments thetransport:argument ofquery/ask. (#155)Pathnamewhere a path goes:settings:(a settings file; used to send no--settingsat all),SystemPromptFile#path, thepathof a{ type: 'file' }system prompt Hash, and a plugin path, typed or Hash (these failed at connect withno implicit conversion of Pathname into String).tools:is signedStringas well; the RBS signatures are widened accordingly. (#160)
Changed
docs/cli-installer.mdnow says whenCLIInstaller::PINNED_CLI_VERSIONmoves: pin-only changes ship with the next gem release rather than one release per CLI bump. It also covers how a gem patch can move your CLI and how to take a newer CLI before a release pins it. The pin-bump workflow keeps a single[Unreleased]entry for the pin.CLIInstaller::PINNED_CLI_VERSIONmoves from 2.1.285 to 2.1.288, following the CLI the Python SDK bundles.CLIInstaller.install_pinnedinstalls it. See the Claude Code changelog.- The Rails guide and both Rails examples turn off the CLI's per-project auto-memory (
env: { 'CLAUDE_CODE_DISABLE_AUTO_MEMORY' => '1' }). Every SDK session reads that memory and aclaude_code-preset session writes it without a permission check, so in a multi-user app one user's "remember …" would reach every other user's session. The generated initializer only documents the switch: it carries the same line commented out, so a newly generated app is not isolated until you uncomment it (or set it per call). If you copied the examples, add the line; the value must be'1'. (#158) docs/configuration.mdhas a "Session Isolation" section. Claude Code's per-project auto-memory is on in SDK sessions: every session loads the project'sMEMORY.mdindex into its context (setting_sources: []does not prevent it), and a session on theclaude_codepreset also writes memory files, which the CLI allows without any permission setup. Servers and multi-tenant hosts should setenv: { 'CLAUDE_CODE_DISABLE_AUTO_MEMORY' => '1' }(orsettings: { autoMemoryEnabled: false }); a custom transport has to pass either one through to its CLI itself;Client#context_usage[:memoryFiles]shows what a session loaded. (#155)- A requested sandbox that is not active now produces a warning.
sandbox: { enabled: true }does not guarantee a sandbox: when the CLI cannot start one (a Linux container withoutbubblewrapandsocat, for example) it prints⚠ Sandbox disabled: …to its stderr and runs commands unsandboxed, which a host only saw through astderr:callback. The SDK now repeats that line as a Ruby warning, prefixed[claude-agent-sdk], once per session, when thesandbox:option enabled the sandbox. The default is unchanged; setfail_if_unavailable: trueto make the CLI exit instead. (#156) OTelObservernow leaves a span for a session that fails before its firstInitMessage. A CLI that could not be found or started, or aninitializethat failed or timed out, raised to the caller but exported no span at all (0.18.0's "crashed sessions now produce OTel traces with error status" only held once anInitMessagehad arrived).on_errornow emits aclaude_agent.sessionspan with the observer's default attributes, the prompt if one had been sent, the exception event and error status, and ends it at once. The span has no model orsession.id. Nothing changes once a trace has started. If you countclaude_agent.sessionspans, failed starts are now among them. (#148)docs/observability.mdno longer promises "standardgen_ai.*semantic conventions … any OTel backend". The attributes follow the Langfuse and OpenInference conventions plus a subset of OTelgen_ai.*; the page now lists every attribute, event and statusOTelObserversets, and how thegen_ai.*names differ from the OTel GenAI semantic conventions — notably thatgen_ai.usage.input_tokensleaves cache tokens out, and that the inclusive count isllm.token_count.prompton the session span. No attribute changed. (#148)docs/rails.mdno longer says callbacks inherit the request's state. A callback runs on a thread or fiber Rails never set up:Currentattributes,Time.zone, log tags, the error context and theconnected_torole / shard read as their defaults there (a write underconnected_to(shard:)lands in the default shard), with or withoutRailtie.callback_wrapper;I18n.localedepends on the i18n version. The guide now has the table, a per-call recipe that carries the state across, the rule for composing your own wrapper with the Rails one, a section on transactions and the connection pool (a callback is outside the caller's transaction and uses its own connection), and a note that in development an agent run holds the reload lock. (#158)docs/configuration.mdsays what the CLI does with an invalid sandbox value: it discards the whole--settingsvalue, sandbox andpermissionsrules alike. The SDK does not surface this —connectsucceeds and nothing appears on stderr; only the CLI'sget_settingscontrol response lists the error. That holds for every form, the typed classes included. (#160)- Disk listings (
list_sessions,get_session_info) scan each transcript's head and tail windows as bytes; with non-ASCII text in a window this takes a fraction of the CPU time it did. Results are unchanged. (#157) - Development only: the gemspec's development dependency on Bundler is
>= 2.0(was~> 2.0), sobundle installworks in a checkout on Ruby 4.0, which ships Bundler 4. CI now runs the suite and RuboCop on Ruby 4.0, and a keyless smoke runs the pinned CLI on PRs that touch the installer, the transport,Queryor the message parser; the API-key integration job shows as skipped instead of passed when the key is absent. Runtime dependencies are unchanged. (#153)
Fixed
- A hook callback's Hash return value now means what the equivalent typed output means, whatever its key style. Only five top-level keys were renamed before, and only as Symbols, so
{ hook_specific_output: { hook_event_name: 'PreToolUse', permission_decision: 'deny' } }reached the CLI under keys it does not read: the decision was ignored and the tool ran. A String-keyed Hash (e.g. afterJSON.parse) and aSyncHookJSONOutputholding a snake_case Hash failed the same way. The fields the typed output classes model are now accepted in snake_case or camelCase, as Symbols or Strings, at the top level and insidehook_specific_output; any other key is sent as written, and the keys insideupdated_input, the tool outputs and aPermissionRequestdecisionare not renamed. If a hook of yours returns snake_case keys, they now take effect. When one Hash spells a field both ways the CLI's spelling is sent, and a Symbol and a String spelling the same key are written once (json 3.x used to fail the hook withdetected duplicate key). (#149) - A
sandbox:Hash written in Ruby spelling is now applied. The typed sandbox classes take snake_case attributes and write the camelCase keys the CLI reads, but a Hash was forwarded as written:sandbox: { enabled: true, network: { denied_domains: ['evil.example'] }, filesystem: { deny_read: ['~/.aws'] } }reached the CLI under keys it does not know, and the CLI ignored them without an error — the session ran withnetwork: {}andfilesystem: {}, no deny rule.SandboxSettings.new(network: { denied_domains: [...] })lost its rules the same way. The fields ofSandboxSettings,SandboxNetworkConfigandSandboxFilesystemConfigare now accepted in snake_case or camelCase, as Symbols or Strings, at the top level and insidenetwork/filesystem; any other key is sent as written. If a sandbox Hash of yours has snake_case keys, they now take effect. A snake_case key is renamed only when its value has the shape the CLI accepts for it (true/false, an Array of Strings, a port number from 0 to 65535); with any other value it is sent as written and stays without effect, as before — so the rename can never make the CLI discard your settings. When one Hash spells a field both ways...
v1.1.0
Syncs with Python SDK 0.2.162. Additive only: one new option and a fix to when query() closes stdin.
Added
ClaudeAgentOptions#verbatim_prompts(defaultfalse; Python SDK #1269). Whentrue, every user message the SDK writes carriesclient_composed: true: String prompts and each message of a streamed prompt, throughquery,Client#connectandClient#query. Claude Code then delivers the text as written, with no@pathfile expansion and no slash-command dispatch. Use it when a prompt includes text your end user did not type.tools: []anddisallowed_toolsdo not stop@pathexpansion, which reads the file before the model runs. While the option is on, a caller'sclient_composedkey is overwritten, and caller Hashes are never mutated. A streamed JSONL String is parsed and re-serialized; one that is not a single JSON object raisesArgumentErrorrather than going out unmarked.Clientreads the option once, atconnect. On current CLIs such a turn also skips the turn-start attachment pass (nestedCLAUDE.mdand rules files, skill and tool listings). Requires Claude Code 2.1.248 or later; the SDK warns when it connects to an older CLI with the option on. Seedocs/configuration.md.- Development only, nothing ships in the gem: TLA+ models in
formal/tla/of the two most concurrency-sensitive designs.CLIInstaller.tlacoversCLIInstaller.install's publish order, the in-lock dist-tag resolve and the stale-temp sweep.ControlProtocol.tlacovers the outbound control-request waiter protocol inQuery.formal/tla/run.shmodel-checks both with TLC. It confirms that the shipped design passes and that each alternative the source comments reject, including the historical rename-then-record order, produces a counterexample.
Changed
CLIInstaller::PINNED_CLI_VERSIONmoves from 2.1.280 (the 1.0.0 pin) to 2.1.285, the CLI the Python SDK 0.2.162 bundles.CLIInstaller.install_pinnedinstalls it. It honors bothclient_composed(verbatim_prompts) andCLAUDE_CODE_SDK_READS_SESSION_STATE(the fix below).
Fixed
query()with hooks,can_use_toolor SDK MCP servers no longer closes stdin while a follow-up turn is still owed (Python SDK #1279, fixing Python issue #1190). A background subagent that finished just before the turn's result was already off the in-flight ledger, so stdin closed at that result, and the follow-up turn its completion triggered had every hook, permission and SDK MCP request fail with "Stream closed" (the model reported the tool as refused). The transport now setsCLAUDE_CODE_SDK_READS_SESSION_STATE=1unlessoptions.envor the environment already names it (in any case; anilvalue inoptions.envunsets it). The CLI then sendssession_state_changedframes markedsdk_host_only, which the SDK drops from the message stream forquery()andClientalike.query()keeps stdin open until the CLI reportsidleafter a result:- The wait between turns is bounded by
CLAUDE_CODE_PRINT_BG_WAIT_CEILING_MS, read fromoptions.envand then the environment; the default is 10 minutes and0means no limit. The clock restarts at each result and at eachrunning. A main-thread turn, the CLI reportingrequires_action, a tracked background agent and a hook, permission or SDK MCP request the SDK is still answering each stop it. - With an Enumerable prompt, each message written waits for its own run. Work the CLI takes up after the run ended reopens it.
- A CLI that sends no state (2.1.282 and earlier) gets the old behavior: stdin closes at the first result with no tracked task in flight.
- Background agents, bounded monitors and MCP tasks can now keep a one-shot run open, up to the ceiling. Background shells, persistent monitors and remote agents do not, because the CLI leaves them out of its
runningreport. SessionStateChangedMessagereaches your code only if you setCLAUDE_CODE_EMIT_SESSION_STATE_EVENTS=1yourself; the SDK never sets it.- A custom transport must pass
CLAUDE_CODE_SDK_READS_SESSION_STATE=1to the CLI itself to get this behavior (seedocs/client.md); the E2B example transport now does.
- The wait between turns is bounded by
Usage
# Prompt text your end user did not type can no longer pull local files in via @path
options = ClaudeAgentSDK::ClaudeAgentOptions.new(verbatim_prompts: true)
ClaudeAgentSDK.query(prompt: untrusted_text, options: options) { |message| ... }What's changed
- #145 Sync with Python SDK 0.2.162: verbatim_prompts + stdin open until idle — @ya-luotao
- #144 Bump PINNED_CLI_VERSION to 2.1.285 — @github-actions
- #143 Bump PINNED_CLI_VERSION to 2.1.284 — @github-actions
- #142 Bump PINNED_CLI_VERSION to 2.1.283 — @github-actions
- #141 Add TLA+ models of the CLI installer and control-request protocol — @ya-luotao
Full Changelog: v1.0.0...v1.1.0
gem install claude-agent-sdk -v 1.1.0
v1.0.0
1.0 is a stability commitment. From here on the public API — everything documented in docs/ plus the YARD docs without @api private, now also described by the RBS signatures in sig/ — follows Semantic Versioning: no breaking changes within 1.x. Upgrading from 0.x? Read UPGRADING-1.0.md and run your suite on 0.37 first.
1.0 (#126): three breaking changes, the first two of which 0.37 warns about at the call site. Read UPGRADING-1.0.md before upgrading and run your suite on 0.37 with warnings visible first: an app that runs on 0.37 without SDK warnings is unaffected by those two, except that respond_to? on a camelCase non-attribute (msg.respond_to?(:toH)) silently answered true on 0.37 and answers false now.
Added
ClaudeAgentSDK::SessionStoreError(aClaudeSDKError), raised byquery,askandClient#connectwhen resuming fromsession_store:fails: a store call (#load,#list_sessions,#list_subkeys) raised or exceededload_timeout_mswhile the SDK materialized the transcript. The message names the store call, and#causeholds the adapter's exception (or the timeout). Documented indocs/errors.mdanddocs/sessions.md.- RBS signatures for the public API (#126). The gem now ships
sig/, so Steep and other RBS tools type-check code that uses it (throughrbs collection;sig/manifest.yamldeclares the one stdlib dependency,pathname). The signatures cover the whole public surface (the objects documented indocs/and YARD without@api private):ClaudeAgentSDK.query/.ask/.configure/.offload, the SDK MCP helpers, every session function (includingsession_store:and the deprecated twins),Clientwith its Ruby-style aliases,ClaudeAgentOptions(every option as a typed keyword), the message, content-block, hook, permission and MCP types, the error hierarchy,CLIInstaller,Streaming,Railtie.callback_wrapperand the observers. Duck types are interfaces, so any object with the right methods fits:_Transport,_SessionStore(only#appendand#loadare required), and one interface per user callback (_CanUseTool,_HookCallback,_ToolHandler,_CallbackWrapper, ...). Hashes follow the documented key rule:wire_hash(Hash[Symbol, untyped]) for data from the CLI stream,transcript_hash(Hash[String, untyped]) for transcripts and store data. Internals tagged@api privatehave no signatures. CI validates the signatures (rake rbs:validate) and runs the suite under RBS's runtime type checker (rake rbs:test), so a signature that disagrees with the code fails the build.CONTRIBUTING.mdexplains how to keepsig/in step with API changes.
Changed
- Breaking: unknown keys on user-constructed types raise
ArgumentError..newand#[]=on the value types you build and pass in (option values such asAgentDefinition,SandboxSettings, the thinking and MCP server configs;HookMatcher; hook outputs;PermissionResultAllow/PermissionResultDeny;PermissionUpdate;PermissionRuleValue) raise instead of warning, with a message naming the class, the key and the known keys:ClaudeAgentSDK::HookMatcher: unknown attribute :matchr (known: hooks, matcher, timeout). camelCase and String keys and a type's own discriminator are still accepted;.from_hash,.wrapand every type parsed from CLI output stay lenient. - Breaking:
Type#[],#[]=and camelCase methods reach attributes only. A name that is not an attribute behaves like an undefined one:msg[:to_h]isnil,#[]=ignores it (raises on the strict types above),msg.toHraisesNoMethodErrorandrespond_to?(:toH)isfalse. Methods your own code adds to a subclass, mixin or instance still count as attributes. - Breaking: store-backed resume failures raise
SessionStoreErrorinstead ofRuntimeError. The two SDK-raisedRuntimeErrors (store call failed, store call timed out) becomeSessionStoreError, and aRuntimeErrorraised by the adapter itself, which 0.37 let through unwrapped, is now wrapped too, sorescue ClaudeSDKErrorcatches every resume-materialization failure. Code that rescuedRuntimeErrorthere must rescueSessionStoreError. The failure message now includes the adapter exception's class (... failed during resume materialization: IOError: connection reset). UPGRADING-1.0.mdships in the gem and the YARD docs, linked from the README.- The deprecation warning printed by the ten
*_from_store/*_via_storesession functions, and their YARD and docs, now say they will be removed in 2.0. 0.36 and 0.37 said 1.0, but they stay, deprecated, for all of 1.x (#126). Nothing else about them changes: each still works and still warns once per process.
Upgrading from 0.x
- Upgrade to
~> 0.37.0and run your suite with warnings visible — every call that 1.0 rejects prints a warning pointing at your code. - Fix those, check any
rescue RuntimeErroraround store-backed resume (nowSessionStoreError). gem 'claude-agent-sdk', '~> 1.0'.
Full guide: UPGRADING-1.0.md
What's changed
- #138 1.0: strict attribute keys, attribute-only access, SessionStoreError — @ya-luotao
- #139 RBS signatures for the public API — @ya-luotao
The road here: #126 (0.35 Rails integration → 0.36 API consolidation → 0.37 deprecations → 1.0).
Full Changelog: v0.37.0...v1.0.0
gem install claude-agent-sdk -v 1.0.0
v0.37.0
The last 0.x release before 1.0 (roadmap). No runtime behaviour changes — only new warnings for things 1.0 will reject. Run your suite on 0.37 with warnings visible before moving to 1.0:
- A misspelled key on a value type you build (
HookMatcher,AgentDefinition,SandboxSettings, MCP server configs, permission results, hook outputs, …) now warns once, pointing at your call; 1.0 raisesArgumentError. msg[:name]/ camelCase readers reaching a non-attribute method (e.g.msg[:to_h]) warn once; in 1.0 they only reach attributes.- The 1.0 SemVer surface is now visible: SDK internals are tagged
@api privateand hidden from the API docs, and the documented contracts (Hash-key rule,mcp_status/context_usagereturn Hashes,Type#[]) are written down.
Changed
- Docs: pre-1.0 contracts written down (#126). No code changes.
Client#mcp_status/#get_mcp_statusand#context_usage/#get_context_usagereturn the CLI's raw Hash with camelCase Symbol keys.docs/types.mdhad describedMcpStatusResponseas the response type; it is the optional typed view, viaMcpStatusResponse.parse(client.mcp_status), and there is no typed context-usage class (docs/client.md). A new "Hash keys" section indocs/types.mdstates one rule: Hashes passed through from the live CLI stream (origin,usage,model_usage, hooktool_input,can_use_toolinput, SDK MCP tool args, control responses) have Symbol keys spelled as on the wire, and Hashes read from transcripts or aSessionStore(SessionMessage#message,get_subagent_metadata, store keys and entries) have String keys. The docs, examples and bundled skill drop theirh[:key] || h['key']fallbacks in favour of the one correct form.Type#[],#[]=and the camelCase readers are documented as public API, including that#[]=changes the object you received.docs/sessions.mddocumentsClaudeAgentSDK.project_key_for_directoryandClaudeAgentSDK.fold_session_summaryfor adapter authors, andimport_session_to_store's batching (batch_size:defaults to 500 entries, and a batch also ends at about 1 MiB). - SDK internals are now tagged
@api privateand hidden from the YARD docs (#126). The tagged namespaces areQuery,MessageParser,TranscriptMirrorBatcher,Sessions,SessionMutations,SessionResume,MaterializedResume,SessionSummary,SessionStores,OptionWarningsandFiberBoundary, including everything nested in them. Some public classes also have internal members, and those are tagged individually:SubprocessCLITransport's methods outside theTransportinterface and its constants;CommandBuilder's constants (.newand#buildstay public);CLIInstaller::Http,Platform,ReleaseandMetadataand the installer's constants other thanPINNED_CLI_VERSION;SdkMcpServer#handle_json,#handle_messageandSdkMcpServer::ToolInputSchema;Client#query_handler; andClaudeAgentSDK::OBSERVER_INTERFACE. Their public faces are unchanged:ClaudeAgentSDK.offload,fold_session_summary, the session functions andSDKSessionInfo/SessionMessageremain public. Nothing changes at runtime. No method or constant was renamed, removed or made private, so every existing call keeps working. From 1.0, SemVer coversdocs/and the YARD API without@api private; objects tagged@api privateare not covered and may change in any release.CONTRIBUTING.mdhas a new "What is public API" section. lib/claude_agent_sdk/types.rbis split into one file per area underlib/claude_agent_sdk/types/(messages, hooks, options, ...). The code moved without edits andtypes.rbstill loads every part, sorequire 'claude_agent_sdk/types'and every class and constant are unchanged; no API change (#126).Type's internal machinery is tagged@api privateand leaves the rendered docs:Type#dup_for_options,Type.deep_dup_for_options,Type::OptionValue,Type.inspect_filtered,Type.inspect_filtered_attributes, and the newType.strict_attributes/.strict_attributes?/.declare_attributes/.attribute?/.attribute_names. They are not part of the SemVer surface.Type#[],#[]=, the camelCase readers,#to_h,.wrapand.from_hashstay public (#126).
Deprecated
- Unknown keys on user-constructed types. The value types you build and pass in (option values such as
AgentDefinition,SandboxSettings, the thinking configs and the MCP server configs;HookMatcher; hook outputs;PermissionResultAllow/PermissionResultDeny;PermissionUpdate;PermissionRuleValue) silently dropped a misspelled key, soHookMatcher.new(matchr: 'Bash')quietly built a matcher with nomatcherset..newand#[]=now warn once per class and key, at the caller's line:ClaudeAgentSDK::HookMatcher: unknown attribute :matchr ignored; this will raise ArgumentError in 1.0 (known: hooks, matcher, timeout). In 1.0 they raiseArgumentError, asClaudeAgentOptionsalready does. camelCase and String keys and a type's own discriminator (type,hook_event_name,behavior) are accepted, soklass.new(value.to_h)round-trips silently. Types parsed from CLI output (messages, content blocks, hook inputs) and every construction through.from_hash/.wrapstay lenient, so a newer CLI's extra fields never warn; permission suggestions from the CLI are now hydrated throughPermissionUpdate.wrapfor that reason. Full list indocs/types.md(#126). Type#[],#[]=and camelCase methods reaching something other than an attribute. They are public API for a type's attributes (itsattr_*fields, the backward-compatibleRateLimitEvent#data, predicates such asoptions.forkSession?, and any method your own code adds to a subclass, mixin or instance), but they resolved any public method:msg[:to_h]returned a Hash,msg['freeze']froze the message,msg.toHcalled#to_h. Such a call still works but warns once per class and name, e.g.ClaudeAgentSDK::ResultMessage#[]: :to_h is not an attribute; Type#[] will only read attributes in 1.0. In 1.0 a name that is not an attribute behaves like an undefined one:#[]returnsnil,#[]=ignores it (raises on the types above), a camelCase call raisesNoMethodError, andrespond_to?saysfalse. Names that are not methods at all keep today's behaviour and do not warn.UserMessage#text/AssistantMessage#textare convenience methods, not attributes (#126).
Check your app before 1.0
# warnings are on by default; make sure your test runner doesn't silence them
bundle exec rspec 2>&1 | grep -E "unknown attribute|is not an attribute|deprecated"What's changed
- #133 Split types.rb into lib/claude_agent_sdk/types/ (no API change) — @ya-luotao
- #136 Warn on unknown keys and non-attribute access on Type (0.37; enforced in 1.0) — @ya-luotao
- #134 Tag leaked SDK internals @api private — @ya-luotao
- #135 Write down the pre-1.0 contracts — @ya-luotao
- #137 Turn Metrics back on everywhere; delete .rubocop_todo.yml — @ya-luotao
Roadmap to 1.0: #126
Full Changelog: v0.36.0...v0.37.0
gem install claude-agent-sdk -v 0.37.0