A Ruby SDK for the Claude Code agent runtime, built for running agents in production Ruby and Rails apps. It has the same capabilities as the official TypeScript and Python SDKs, plus what a Rails deploy needs around them: a generator and CLI-vendoring rake task, callbacks that are safe to touch ActiveRecord from, a pinned CLI binary, built-in OpenTelemetry tracing, and transcript mirroring to your own storage.
Unofficial and community-maintained. This project is not affiliated with or supported by Anthropic. It tracks the official SDKs release by release; see the CHANGELOG for the currently synced version.
Upgrading from 0.x? 1.0 raises on unknown keys, limits
#[]to attributes and addsSessionStoreError. UPGRADING-1.0.md has the checklist.
- Rails integration.
bin/rails generate claude_agent_sdk:installwrites the initializer andbin/rails claude_agent_sdk:install_clivendors the CLI; docs/rails.md covers jobs, ActionCable streaming, session resumption, and solid_queue fiber workers (callback_scheduling: :inline). - Callbacks that are safe around ActiveRecord. Tool handlers, hooks, permission callbacks, and message blocks run on a plain thread by default, outside the SDK's fiber scheduler, so thread-keyed libraries (ActiveRecord,
pg, per-thread caches) behave as they do everywhere else in your app.ClaudeAgentSDK::Railtie.callback_wrapperruns them in the Rails executor so connections go back to the pool, without deadlocking development code reloading. - Hermetic deploys.
CLIInstallervendors a checksum-verified CLI binary, pinned to the version each gem release is tested with, so production never depends on a globalnpm install. - Built-in OpenTelemetry observer with Langfuse support; no third-party instrumentation library required.
- Transcript mirroring. A
SessionStoreadapter mirrors session transcripts to your own storage (reference adapters for Postgres, Redis, and S3, plus a conformance suite), and sessions can be resumed from it on another host. - Same wire protocol as the official SDKs. Spawns the
claudeCLI as a subprocess and speaks stream-JSON over stdin/stdout, so every feature of the runtime is available: sessions, subagents, sandboxing, structured output, file checkpointing and rewind. query()for one-shot calls,Clientfor bidirectional sessions with interrupts, mid-session model switching, and streaming input from anyEnumerator.- In-process custom tools. Define tools as Ruby blocks; they run inside your process with direct access to your app state (SDK MCP servers), with JSON-Schema-validated arguments.
- All 27 hook events and permission callbacks with typed inputs, so you can gate, audit, or rewrite every tool call.
- Pluggable transport to run the CLI somewhere else (an E2B microVM, a container, over SSH).
# Gemfile
gem 'claude-agent-sdk', '~> 1.0'Then bundle install, or install directly with gem install claude-agent-sdk. To track unreleased changes, point the Gemfile at GitHub: gem 'claude-agent-sdk', github: 'rubycatco/claude-agent-sdk-ruby'.
Prerequisites
- Ruby 3.2 or newer
- Credentials for Claude Code:
ANTHROPIC_API_KEY,CLAUDE_CODE_OAUTH_TOKEN, or a login the CLI has already stored (see Authentication) - Claude Code CLI 2.0.0 or newer, either installed globally (
npm install -g @anthropic-ai/claude-code) or vendored withCLIInstaller:
# bin/setup or a cached Docker layer: the CLI version this gem release was tested with
ClaudeAgentSDK::CLIInstaller.install_pinned # => "/app/vendor/claude/claude"install_pinned installs CLIInstaller::PINNED_CLI_VERSION, so upgrading the gem carries the CLI forward with it; CLIInstaller.install(version: 'x.y.z') pins a version of your own. The vendored binary is found ahead of PATH, installs are idempotent and concurrency-safe, and a failed upgrade never breaks a working install. See docs/cli-installer.md for the full behaviour, supported platforms, and the CLI discovery order.
The SDK holds no credentials of its own. The claude process it starts authenticates the way Claude Code does, with one of:
ANTHROPIC_API_KEY, in the environment of your Ruby process (the CLI inherits it) or per session withClaudeAgentOptions.new(env: { 'ANTHROPIC_API_KEY' => key })CLAUDE_CODE_OAUTH_TOKEN, a long-lived token for a Claude subscription (claude setup-tokencreates one), set the same way- a login the CLI has already stored for the user your process runs as (
claude auth login)
A machine with none of them, such as a fresh container or a CI runner, does not fail at startup. The first prompt comes back as an AssistantMessage whose error is 'authentication_failed' (its text is "Not logged in · Please run /login", a command an SDK host cannot run), followed by a ResultMessage with is_error set. query() and ask then raise ResultError with terminal_reason == 'api_error'; a Client session stays open, so check the result's is_error there. A key or token the API rejects ends the same way, with api_error_status 401, but only after the CLI has retried: watch for APIRetryMessage (ten of them over about three minutes when tested) rather than waiting for the error. docs/errors.md shows how to handle both.
bundle add claude-agent-sdk
bin/rails generate claude_agent_sdk:install # config/initializers/claude_agent_sdk.rb + .gitignore entry
bin/rails claude_agent_sdk:install_cli # the tested CLI into vendor/claude (also a Docker build step)# app/jobs/summarize_ticket_job.rb
class SummarizeTicketJob < ApplicationJob
def perform(ticket)
options = ClaudeAgentSDK::ClaudeAgentOptions.new(tools: [], max_turns: 1)
prompt = "Summarize this support ticket in two sentences:\n\n#{ticket.body}"
ClaudeAgentSDK.query(prompt: prompt, options: options) do |message|
ticket.update!(summary: message.result) if message.is_a?(ClaudeAgentSDK::ResultMessage)
end
end
endThe block runs on a plain thread, so ActiveRecord calls inside it just work. docs/rails.md continues with multi-turn sessions, ActionCable streaming, and fiber workers.
require 'claude_agent_sdk'
puts ClaudeAgentSDK.ask("What is 2 + 2?").resultask runs the whole conversation and returns the final ResultMessage: #result is the answer, and the same object carries total_cost_usd, usage, session_id and structured_output (puts on it prints a summary such as [result: success, 1 turn, 2.1s, $0.0031]). It takes the same prompt and options: as query(), and given a block it also yields every message as it arrives:
result = ClaudeAgentSDK.ask("Explain Ruby's GVL in three sentences") do |message|
puts message.text if message.is_a?(ClaudeAgentSDK::AssistantMessage)
end
puts resultIt raises the same errors as query() (see docs/errors.md), plus CLIConnectionError if the stream ends without a result.
query() runs a single conversation and yields each response message to the block. Reach for it over ask when you handle the messages yourself, or want to stop early with break.
options = ClaudeAgentSDK::ClaudeAgentOptions.new(
system_prompt: "You are a helpful assistant",
allowed_tools: ['Read', 'Write', 'Bash'],
permission_mode: 'acceptEdits',
cwd: "/path/to/project",
max_turns: 5
)
ClaudeAgentSDK.query(prompt: "Create a hello.rb file", options: options) do |message|
puts message
endPass an Enumerator instead of a string to stream several user messages into one session:
stream = ClaudeAgentSDK::Streaming.from_array(['Hello!', 'What is 2+2?', 'Thanks!'])
ClaudeAgentSDK.query(prompt: stream) do |message|
puts message if message.is_a?(ClaudeAgentSDK::AssistantMessage)
endClient keeps a session open so you can send follow-up queries, interrupt, and switch the model or the permission mode mid-session. (Hooks, permission callbacks and custom tools are not a reason to choose it: they work with query() and ask too.) Client.open connects, yields the client, and always disconnects when the block exits, even on an exception. It returns the block's value.
require 'claude_agent_sdk'
ClaudeAgentSDK::Client.open do |client|
client.query("What is the capital of France?")
client.receive_response { |msg| puts msg }
client.query("And of Germany?")
client.receive_response { |msg| puts msg }
endClient.open creates an async reactor when there isn't one; blocking calls yield automatically, no await needed. Called outside a reactor, break inside the block raises LocalJumpError (the client still disconnects), so return a value instead. Code that is already running inside an Async reactor can also manage the lifecycle by hand:
client = ClaudeAgentSDK::Client.new
begin
client.connect
client.query("What is the capital of France?")
client.receive_response { |msg| puts msg }
ensure
client.disconnect
endSee docs/client.md for interrupt, mid-session model and permission switching, MCP status, and custom transports.
Tools are Ruby blocks that run in-process, with no subprocess or IPC between Claude's tool call and your code.
greet = ClaudeAgentSDK.create_tool('greet', 'Greet a user', { name: :string }) do |args|
"Hello, #{args[:name]}!"
end
server = ClaudeAgentSDK.create_sdk_mcp_server(name: 'my-tools', tools: [greet])
options = ClaudeAgentSDK::ClaudeAgentOptions.new(
mcp_servers: { tools: server },
allowed_tools: ['mcp__tools__greet']
)A String return is sent to Claude as a single text block. Return a Hash instead ({ content: [...], is_error: true }) to flag an error, attach structured_content:, or send several content blocks or images. Arguments are validated against the tool's JSON Schema before your handler runs, and handler exceptions are reported back to the model in-band so it can self-correct. See docs/mcp-servers.md for resources, prompts, mixed SDK + external servers, and schema details.
Hooks run your Ruby code at any of the 27 lifecycle events (PreToolUse, PostToolUse, UserPromptSubmit, Stop, PreCompact, …) with typed inputs. Permission callbacks decide programmatically whether a tool call may proceed.
options = ClaudeAgentSDK::ClaudeAgentOptions.new(
hooks: { 'PreToolUse' => [ClaudeAgentSDK::HookMatcher.new(matcher: 'Bash', hooks: [my_hook])] },
can_use_tool: my_permission_callback
)See docs/hooks-and-permissions.md for the full event list and worked examples.
| Topic | Guide |
|---|---|
Client advanced features and custom transports |
docs/client.md |
| SDK MCP servers: tools, resources, prompts, schema compatibility | docs/mcp-servers.md |
| All hook events, typed inputs, permission callbacks | docs/hooks-and-permissions.md |
| Structured output, thinking, budget, fallback and advisor models, sandbox, bare mode, session isolation, checkpointing | docs/configuration.md |
Every ClaudeAgentOptions attribute: type, default, and the CLI flag or protocol field it becomes; environment variables |
docs/options.md |
| Session listing, reading, renaming, tagging, forking, resume-at-message | docs/sessions.md |
| Subagent capabilities, event contracts, and minimal example | docs/subagents.md |
| OpenTelemetry tracing, Langfuse, custom observers | docs/observability.md |
Rails: generator, install_cli task, callback wrapper, fiber safety, solid_queue fiber workers, ActionCable, jobs |
docs/rails.md |
| Vendoring a pinned CLI binary and CLI discovery order | docs/cli-installer.md |
| Hash-key rule, attribute access, and the message, content block, and configuration type reference | docs/types.md |
| Error handling, exception hierarchy, timeouts | docs/errors.md |
API reference: rubydoc.info/gems/claude-agent-sdk. Available built-in tools: Claude Code documentation.
Runnable scripts live in examples/.
| Area | Examples |
|---|---|
| Getting started | quick_start · client · streaming_input · message_types · error_handling |
| Sessions and output | session_resumption · structured_output · extended_thinking · session_stores/ |
| Tools and MCP | mcp_calculator · mcp_resources_prompts · http_mcp_server |
| Hooks and permissions | hooks · advanced_hooks · lifecycle_hooks · permission_callback |
| Models and limits | budget_control · fallback_model · advisor · bare_mode · sandbox |
| Rails, observability, transports | rails_actioncable · rails_background_job · otel_langfuse · e2b_transport |
All three SDKs drive the same CLI over the same protocol, so capabilities line up feature for feature. Ruby differs mainly in idiom: Enumerator for streaming input, blocks for tools, and the async gem with fibers instead of async/await.
| Capability | TypeScript | Python | Ruby (this gem) |
|---|---|---|---|
One-shot query() |
✅ | ✅ | ✅ |
Bidirectional Client |
✅ | ✅ | ✅ |
| Streaming input | AsyncIterable |
AsyncIterable |
Enumerator |
| Custom tools (SDK MCP servers) | tool() |
@tool decorator |
create_tool block |
| Hooks (all 27 events) | ✅ | 10 typed, the rest by name | ✅ |
| Permission callbacks | ✅ | ✅ | ✅ |
| Structured output | ✅ | ✅ | ✅ |
| All 28 message types | ✅ | partial | ✅ |
| Sandbox settings | ✅ | partial | ✅ |
Bare mode (--bare) |
✅ | via extra_args |
✅ |
| File checkpointing & rewind | ✅ | ✅ | ✅ |
| Session browsing & mutations | ✅ | ✅ | ✅ |
| Programmatic subagents | ✅ | ✅ | ✅ |
| CLI binary | bundled | bundled | vendored on demand (CLIInstaller) |
| Observability (OTel / Langfuse) | via Arize | — | ✅ built-in |
| Custom transport (pluggable I/O) | — | ✅ | ✅ |
| Rails integration | — | — | ✅ |
Types are plain Ruby classes with attr_accessor and keyword arguments, mirroring the field names of the TypeScript Zod schemas and Python dataclasses; there is no runtime type checking.
This repository is also a Claude Code plugin marketplace. The bundled skill teaches Claude Code the gem's APIs and patterns:
/plugin marketplace add rubycatco/claude-agent-sdk-ruby
/plugin install claude-agent-ruby@claude-agent-sdk-rubybundle install
bundle exec rspec # unit suite
bundle exec rubocop # lint
bundle exec rake rbs:validate # validate the RBS signatures in sig/
bundle exec rake rbs:test # the suite under RBS runtime type checking
RUN_INTEGRATION=1 bundle exec rspec # also run the real-CLI integration suite (needs `claude` and ANTHROPIC_API_KEY)
BUNDLE_GEMFILE=gemfiles/rails_8.gemfile bundle exec rspec --options spec/rails/.rspec # Rails integration specsCI runs the suite and RuboCop on Ruby 3.2, 3.3, 3.4 and 4.0 on Linux, the suite on macOS, and the Rails specs against Rails 7.1 and 8, validates the RBS signatures and runs the suite under RBS runtime type checking. Weekly, and on PRs that touch the installer, the transport, Query or the message parser, it runs a keyless smoke test against the pinned CLI, and the integration suite as well when the repository has an API key. The gem ships RBS signatures for its public API in sig/, which Steep and other RBS tools pick up through rbs collection. See CONTRIBUTING.md for the development setup and spec/README.md for the test layout.
Bug reports and pull requests are welcome on GitHub. Please include a failing spec with bug reports where possible, and keep pull requests focused on one change; CONTRIBUTING.md has the details. Report security vulnerabilities privately, as described in SECURITY.md. Releases follow Semantic Versioning and are recorded in the CHANGELOG.
Released under the MIT License.
