Repository navigation
v0.36.0
The first step on the road to 1.0: one session_store: argument for every session function (the store-specific twins are deprecated), ClaudeAgentSDK.ask, String tool results, and the last audit follow-ups (#119–#121). Read before upgrading:
- An
exit,Interruptor signal raised inside a hook,can_use_toolor SDK MCP handler now ends the process after the CLI gets its error response. Since 0.34.0 a tool handler'sexitwas turned into anisErrorresult and the process kept running, which also swallowed a real Ctrl-C or SIGTERM in:inlinemode. - The
*_from_store/*_via_storesession functions print a one-time deprecation warning; switch tolist_sessions(session_store: store)etc. (table under Deprecated). - Local-disk session APIs raise
ConfigDirError(notArgumentError) on hosts without a home directory; a session with no prompt hasfirst_promptnilon the disk path too (was'').
Added
- SDK MCP tool handlers may return a String.
create_tool('greet', ...) { |args| "Hello, #{args[:name]}!" }now sends Claude a single text block, the same as returning{ content: [{ type: 'text', text: "Hello, ..." }] }. Both dispatch paths (tools/callthrough the MCP server and the directSdkMcpServer#call_tool) accept it. Hash returns behave exactly as before and remain the form foris_error,structured_content, images and several blocks; any other non-Hash return still produces the in-band "must return a hash with :content key" error. Thecreate_toolYARD examples, README anddocs/mcp-servers.md(new "Handler Return Values" section) lead with the String form. ClaudeAgentSDK.ask(prompt, options: nil)— runsqueryto completion and returns the finalResultMessage, soClaudeAgentSDK.ask("What is 2 + 2?").resultis the answer text, with cost, usage andsession_idon the same object. It isqueryunderneath: same prompt types (String or Enumerable), sameoptions:andtransport:, same errors (a terminal error exit still raisesResultError; anis_errorresult that is not followed by an error exit is returned like any other). An optional block receives every message as it arrives, so callers can stream progress and still get the result. It consumes the whole stream and returns the lastResultMessage; if the stream ends without one it raisesCLIConnectionError. The README Quick Start now leads with it.- Ruby-style
Clientmethod names next to the Python-parity ones:client.model = 'haiku'(set_model),client.permission_mode = 'plan'(set_permission_mode),client.context_usage(get_context_usage) andclient.mcp_status(get_mcp_status). Each delegates to its parity counterpart, so both spellings send the same control request, raise the sameCLIConnectionErrorbeforeconnect, and an override of the parity method covers both. The parity names stay, so code ported from the Python docs keeps working. (server_info/get_server_infoalready existed as a pair.)docs/client.mdlists both spellings. CLIInstaller.root— a configurable root for the vendored CLI.CLIInstaller.default_dir(and soinstall,install_pinned,installed_pathwithoutdir:, and the transport's discovery of the vendored binary) isvendor/claudeunderCLIInstaller.rootwhen it is set, and under the working directory at call time when it isnil, the default. A process whose cwd is not the project root, such as a daemonized worker, can now find the vendored binary instead of falling through toPATH. The setter takes a String or Pathname, absolutizes it once, and rejects an empty path;nilrestores the working-directory default. Unset, nothing changes, and an explicitdir:still wins.- The Railtie anchors CLI discovery to
Rails.root: an initializer setsCLIInstaller.root ||= Rails.rootbeforeconfig/initializersrun, so an app initializer can override it and a root set inconfig/application.rbis kept. Theclaude_agent_sdk:install_clitask, which does not boot the app, installs under an explicitly setCLIInstaller.rootif there is one, and underRails.root/vendor/claudeas before otherwise. The generated initializer's commentedcli_path:line no longer describes a cwd workaround.docs/cli-installer.mdgains a "Wherevendor/claudeis" section.
Groundwork for 1.0 (#126): the sessions API collapses to one function per operation, and the store-specific twins are deprecated. Nothing is removed; every existing call keeps working. session_store:on every session function.list_sessions,get_session_info,get_session_messages,list_subagents,get_subagent_metadata,get_subagent_messages,rename_session,tag_session,delete_sessionandfork_sessiontake an optionalsession_store:. Omitted ornil, they work on local disk exactly as before; given a store, they run the same code the*_from_store/*_via_storefunction did, with the same arguments. Two differences between the paths are documented indocs/sessions.md:directory: nilmeans every project on disk but the current working directory with a store (a store cannot enumerate projects), andinclude_worktrees:is disk-only: withsession_store:,list_sessionsaccepts only the defaulttrueand raisesArgumentErrorforfalseornilinstead of silently ignoring the filter.ClaudeAgentSDK::ConfigDirError(aClaudeSDKError), raised by the local-disk session APIs when the Claude config directory cannot be located:CLAUDE_CONFIG_DIRis unset and there is no usable home directory for the default~/.claude. Its message says to setCLAUDE_CONFIG_DIR(#120).- CI: a macOS leg (Ruby 3.4) for the main suite; simplecov coverage (
COVERAGE=1 bundle exec rspec) on the Linux Ruby 3.4 leg, with line/branch totals in the job summary and the HTML report as an artifact; and a weekly real-CLI integration run (.github/workflows/integration.yml) againstCLIInstaller::PINNED_CLI_VERSION, also triggered by PRs that touch the installer. Dependabot keeps the workflows' actions current. CONTRIBUTING.md,SECURITY.md, and issue and pull request templates.
Changed
exit/Interrupt/ signal exceptions from an SDK MCP tool handler terminate the process again, after the CLI gets itsisErrorresult. Since #77 (0.34.0) they were converted into anisErrorresult and the process kept running. That also swallowed a real Ctrl-C orSIGTERMlanding in an:inlinehandler. TheisErrortext now names the exception class ("SystemExit: exit"rather than"exit").SdkMcpServer#call_tooland#handle_message, called directly without a session, let these exceptions propagate instead of returning anisErrorresult. (#119)- Root-module plumbing is tagged
@api privateand no longer appears in the generated YARD docs (.yardoptsgains--hide-api private;--no-privatealone never hid@api privateobjects):resolve_observers,extract_sdk_mcp_servers,convert_hooks_to_internal_format,configure_can_use_tool,extract_exclude_dynamic_sections,extract_system_prompt_snapshot,notify_observers,check_inline_isolation,extract_user_prompt_text,prompt_text_from_content,observing_prompt_stream,flexible_fetch,normalize_tool_result, and the tool-schema helpersdeep_symbolize_keys,deep_normalize_schema,prebuilt_json_schema?,normalize_tool_schema,ruby_type_to_json_schema. They still work, but they are not part of the public API and move underClaudeAgentSDK::Internalin 1.0.fold_session_summarystays public, since SessionStore adapters call it from#append. The existing@api privatetags (FiberBoundaryinternals,CancellationSignal#cancel) are hidden too;#cancel's tag is moved to its own line, where YARD recognizes it. examples/rails_actioncable_example.rbandexamples/rails_background_job_example.rbuseClaudeAgentSDK::Client.openinstead of hand-rolledAsync { connect … ensure disconnect }.wait, matchingdocs/rails.md.- RuboCop targets Ruby 3.2, the gemspec floor (was 3.0). The resulting autocorrections (anonymous block forwarding, dropping
require 'set') change no behavior.
Deprecated
-
The ten store-specific session functions, removed in 1.0. Each still returns exactly what it did before (a
nilsession_store:still fails rather than falling back to disk), and prints a one-time warning per method per process naming its replacement and the calling line, e.g.app/jobs/sync.rb:12: warning: ClaudeAgentSDK.list_sessions_from_store is deprecated and will be removed in 1.0; use ClaudeAgentSDK.list_sessions(session_store: store). The warning uses plainKernel#warn, notcategory: :deprecated, because Ruby hides that category unlessWarning[:deprecated]is enabled;-W0/$VERBOSE = nilsilences it.Deprecated Replacement list_sessions_from_store(session_store: s, ...)list_sessions(session_store: s, ...)get_session_info_from_store(session_store: s, ...)get_session_info(session_store: s, ...)get_session_messages_from_store(session_store: s, ...)get_session_messages(session_store: s, ...)list_subagents_from_store(session_store: s, ...)list_subagents(session_store: s, ...)get_subagent_metadata_from_store(session_store: s, ...)get_subagent_metadata(session_store: s, ...)get_subagent_messages_from_store(session_store: s, ...)get_subagent_messages(session_store: s, ...)rename_session_via_store(session_store: s, ...)rename_session(session_store: s, ...)tag_session_via_store(session_store: s, ...)tag_session(session_store: s, ...)delete_session_via_store(session_store: s, ...)delete_session(session_store: s, ...)fork_session_via_store(session_store: s, ...)fork_session(session_store: s, ...)All other arguments carry over unchanged.
import_session_to_storeis not affected.
Fixed
exit,Interruptand signal exceptions raised while a hook,can_use_tool, or an SDK MCP resource reader / prompt generator runs no longer leave the CLI without an answer. The SDK now responds first, then re-raises. The CLI gets exactly the response an ordinary exception from that callback produces, naming the exception class ("SystemExit: exit","Interrupt","SignalException: SIGTERM"). For hooks andcan_use_toolthat is the error control response; forresources/read/prompts/getit is a JSON-RPC-32603error. The transport flushes that response, and then the original exception propagates, so the process terminates as plain Ruby would:exit 3exits with status 3, and Ctrl-C interrupts. Previously no response was written and the reactor stopped. In the default:threadscheduling, Ruby also re-raised the worker thread'sSystemExiton the main thread, which answered with a misleadingCancelled. This covers every dispatch site:can_use_tool, hooks with and without aHookMatcher#timeout(both variants), resources, prompts and tool handlers. It also covers a real Ctrl-C /SIGTERMthat MRI delivers to the main thread while an:inlinecallback runs there, and a:threadhook that callsexitafter its timeout expired (onlyexitis carried out of such an abandoned worker: anInterruptor signal exception it raises ends that thread alone, as in plain Ruby). Cancellation (Async::Stop, hook timeouts) still propagates unchanged. Acallback_wrappersees an internalStandardErrorcarrier whose#causeis the original, so ensure-based wrappers still clean up; the original is re-raised even if the wrapper swallows the carrier. Observers and message blocks are unchanged. (#119)- Session APIs on hosts without a home directory (#120). With
CLAUDE_CONFIG_DIRunset andHOMEunset with no passwd entry (docker --userin a minimal image) or an empty/relativeHOME:- the local-disk session APIs (
list_sessions,get_session_*,list_subagents,rename_session/tag_session/delete_session/fork_session,import_session_to_store) raiseConfigDirErrorinstead of a bareArgumentErrorfrom~expansion; - a fresh session with a
session_storeno longer fails at connect. The transcript mirror cannot map the CLI's transcript files to store keys without a projects dir, so each unmappable batch is reported as aMirrorErrorMessage(with anilkey) and counted as dropped, while the session itself runs normally.SessionStores.projects_dirreturnsnilin this case instead of raising.
- the local-disk session APIs (
- Store-backed resume seeds auth and settings from the home the CLI subprocess will use (#120). When
options.envsetsHOME,.credentials.json,settings.json/cowork_settings.jsonand.claude.jsonare now read from under that home, asCLAUDE_CONFIG_DIRalready was, instead of the parent process's home. An empty or relativeHOMEthere, orHOME => nil, counts as no home, so those files are skipped. The transcript mirror resolves the subprocess's default~/.claude/projectsthe same way, so aHOMEoverride no longer sends every mirror frame down the "not under projects dir" drop path. - Remaining disk/store session read inconsistencies (#121):
- Store listings report
SDKSessionInfo#last_modifiedas Integer epoch milliseconds, as documented, whatever shape the adapter'smtimehas (ISO-8601 String, numeric String, Float, or nowTime, e.g. an ActiveRecordupdated_at). Previously only the ordering was coerced and the raw value leaked through. An unusable mtime (including non-finite numbers) reads as0, the value it already sorted by. continue_conversationwith asession_storebreaks equal-mtime ties bysession_id, like the listings, instead of resuming whichever session the adapter listed first.first_promptisnilon the disk path too (it was'') when a session has no prompt, matching the store path and the Python SDK.cwd: both paths take the first non-blank top-levelcwd. The disk path took the firstcwdeven when empty (then fell back to the project path) and also matchedcwdkeys nested in tool inputs; the store fold now also skips whitespace-only values, so a sidecar no longer locks on one.rename_session/tag_session/delete_session/fork_sessionand their_via_storecounterparts validatesession_id(andup_to_message_id) at the boundary like the readers: a non-String id raisesArgumentError("Invalid session_id") instead ofNoMethodError.list_sessionsdeduplicates a session found in several project directories deterministically: newestlast_modified, then the larger file, then the project directory that sorts first (the global scan now walks project directories in name order). Equal mtimes used to keep whichever copy the filesystem listed first.
- Store listings report
Usage
result = ClaudeAgentSDK.ask('What is 2 + 2?')
puts result.result # the answer text
puts result # [result: success, 1 turn, 2.1s, $0.0031]
greet = ClaudeAgentSDK.create_tool('greet', 'Greet a user', { name: :string }) { |args| "Hello, #{args[:name]}!" }
ClaudeAgentSDK.list_sessions(session_store: store) # was list_sessions_from_store(session_store: store)What's changed
- #128 CI: macOS leg, coverage, scheduled real-CLI run; community files — @ya-luotao
- #131 Tolerate HOME-less hosts and close residual disk/store session inconsistencies (#120, #121) — @ya-luotao
- #129 Consolidate the sessions API behind session_store: and deprecate the store twins — @ya-luotao
- #130 Additive DX: String tool results, ClaudeAgentSDK.ask, Ruby-style Client names, CLIInstaller.root — @ya-luotao
- #127 Answer the CLI, then re-raise exit/Interrupt from user callbacks (#119) — @ya-luotao
Roadmap to 1.0: #126
Full Changelog: v0.35.0...v0.36.0
gem install claude-agent-sdk -v 0.36.0