An unofficial, community-maintained Ruby SDK for the Claude Code agent runtime. Not affiliated with or supported by Anthropic.
Official SDKs: TypeScript · Python.
Ruby powers a massive ecosystem — Rails, Sidekiq, Kamal, countless production web apps — but has no official Claude Agent SDK. This gem fills that gap so Ruby and Rails developers can build AI agents, automate coding workflows, and integrate Claude into existing applications without switching languages or shelling out to Python/Node.
All three SDKs share the same underlying mechanism: they spawn the claude CLI as a subprocess and communicate over stream-JSON on stdin/stdout. The wire protocol is identical, so Ruby gets the same capabilities as the official SDKs.
| 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) | ✅ | ✅ | ✅ |
| Permission callbacks | ✅ | ✅ | ✅ |
| Structured output | ✅ | ✅ | ✅ |
| All 24 message types | ✅ | partial | ✅ |
| Sandbox settings | ✅ | partial | ✅ |
Bare mode (--bare) |
✅ | ✅ | ✅ |
| File checkpointing & rewind | ✅ | ✅ | ✅ |
| Session browsing & mutations | ✅ | ✅ | ✅ |
| Programmatic subagents | ✅ | ✅ | ✅ |
| Bundled CLI binary | ✅ | ✅ | — (install claude separately) |
| Observability (OTel / Langfuse) | via Arize | — | ✅ (built-in) |
| Custom transport (pluggable I/O) | — | — | ✅ |
| Rails integration | — | — | ✅ |
Where Ruby goes further: Built-in OpenTelemetry observer with Langfuse flow diagram support — no third-party instrumentation library needed. Custom transport support lets you swap the subprocess for any I/O layer (e.g., connect to a remote Claude Code instance over SSH or a container). Rails integration provides a configure block for initializers with thread-safe observer factories, and plays well with ActionCable for real-time streaming. Full typed coverage for all 24 CLI message types and all 27 hook events.
What's missing: The Ruby gem does not bundle the claude CLI binary (npm install -g @anthropic-ai/claude-code).
Implementation differences from the official SDKs
TypeScript uses native async/await. Python uses async/await with anyio. Ruby uses the async gem with fibers — no await keyword needed; blocking calls yield automatically inside an Async block:
Async do
client = ClaudeAgentSDK::Client.new(options: options)
client.connect
client.query("Hello")
client.receive_response { |msg| puts msg }
client.disconnect
end.waitTypes use plain Ruby classes with attr_accessor and keyword args — no runtime type checking, but the same structure and field names as the TS Zod schemas / Python dataclasses. Subprocess transport uses Open3.popen3; wire protocol is identical.
Add this line to your application's Gemfile:
# Recommended: use the latest from GitHub for newest features
gem 'claude-agent-sdk', github: 'ya-luotao/claude-agent-sdk-ruby'
# Or use a stable version from RubyGems
gem 'claude-agent-sdk', '~> 0.30.0'Then bundle install, or install directly: gem install claude-agent-sdk.
Prerequisites:
- Ruby 3.2+
- Node.js
- Claude Code 2.0.0+:
npm install -g @anthropic-ai/claude-code
The SDK runs the claude CLI as a subprocess, so a deploy is only reproducible if the CLI version is pinned with it. CLIInstaller downloads a pinned binary from the official release endpoint into a project-local directory (vendor/claude by default) — checksum-verified, no npm/Node at runtime, and nothing extra shipped inside the gem.
require 'claude_agent_sdk'
# 'stable' (default), 'latest', or a concrete version — pin it in production.
ClaudeAgentSDK::CLIInstaller.install(version: '2.1.220')
# => "/app/vendor/claude/claude"
ClaudeAgentSDK::CLIInstaller.install(version: '2.1.220', dir: '/opt/claude')
# nil unless a binary is already installed there
ClaudeAgentSDK::CLIInstaller.installed_pathinstall is idempotent and safe to run concurrently, so it fits bin/setup, a cached Docker layer, and every process of a multi-process boot:
- The install directory's
VERSIONfile records the installed version and the SHA-256 that was verified at download time. The shortcut re-hashes the vendored binary (~0.1s for the real 245MB binary) and only skips the download when both match — a truncated, swapped or half-written binary is reinstalled instead of trusted. It makes no network request, so repeat boots work offline — with a pinned concrete version;'stable'/'latest'must always re-resolve through the endpoint, which is one more reason to pin in production. - An exclusive
flockon<dir>/.install.lockcovers the whole check → download → place → record sequence, so parallel installs into one directory don't race; the loser simply observes the finished install.
Failures (unsupported platform, invalid version, HTTP error, response-size cap, oversized download, checksum mismatch, filesystem errors) raise ClaudeAgentSDK::CLIInstallError.
A failed install never breaks a working one. The new binary is downloaded to a temp file, checksum-verified and recorded, and only then renamed into place — the rename is the last step, and nothing can fail after it. So a failed upgrade leaves the previously installed binary intact and runnable (the SDK keeps working), and the next install redoes it cleanly. A first install that fails leaves nothing behind at all.
The vendored directory is trusted input: anything that can write to it can replace the binary the SDK executes. Keep it inside your deploy artifact, owned by the deploy user and not world-writable, exactly as you would treat
bin/.
# Dockerfile — pin the CLI in its own cached layer
RUN bundle exec ruby -e "require 'claude_agent_sdk'; \
ClaudeAgentSDK::CLIInstaller.install(version: '2.1.220')"#!/usr/bin/env ruby
# bin/setup
require 'claude_agent_sdk'
puts ClaudeAgentSDK::CLIInstaller.install(version: ENV.fetch('CLAUDE_CLI_VERSION', 'stable'))Supported platforms: darwin-arm64, darwin-x64 (Rosetta 2 gets the arm64 build), linux-x64, linux-arm64, and the -musl variants. Windows is not supported.
CLI discovery order. With no explicit cli_path: in ClaudeAgentOptions, the transport probes in this order:
CLAUDE_CLI_PATH— an explicit path to an executable, no discovery at all (a relative value is resolved against the process's working directory, notcwd:)- The vendored binary (
CLIInstaller.installed_path) — deliberately ahead ofPATH, so a pinned install beats whatever is installed globally which claude- Common install locations (
~/.claude/local/claude,/usr/local/bin/claude, …)
If you're using Claude Code, this repo is a Claude Code plugin marketplace. Add it once, then install the skill:
/plugin marketplace add ya-luotao/claude-agent-sdk-ruby
/plugin install claude-agent-ruby@claude-agent-sdk-rubyThis skill teaches your AI coding assistant about the SDK's APIs, patterns, and best practices.
require 'claude_agent_sdk'
ClaudeAgentSDK.query(prompt: "What is 2 + 2?") do |message|
puts message
endquery() is a function for querying Claude Code. It yields response messages to a block.
require 'claude_agent_sdk'
# Simple query
ClaudeAgentSDK.query(prompt: "Hello Claude") do |message|
puts message.text if message.is_a?(ClaudeAgentSDK::AssistantMessage)
end
# With options
options = ClaudeAgentSDK::ClaudeAgentOptions.new(
system_prompt: "You are a helpful assistant",
max_turns: 1
)
ClaudeAgentSDK.query(prompt: "Tell me a joke", options: options) do |message|
puts message
endUsing tools:
options = ClaudeAgentSDK::ClaudeAgentOptions.new(
allowed_tools: ['Read', 'Write', 'Bash'],
permission_mode: 'acceptEdits',
cwd: "/path/to/project"
)
ClaudeAgentSDK.query(prompt: "Create a hello.rb file", options: options) { |message| }Streaming input — send multiple messages dynamically instead of a single prompt string:
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)
endSee examples/streaming_input_example.rb and examples/quick_start.rb.
Client supports interactive conversations with hooks, permission callbacks, and custom tools. It uses streaming mode automatically.
require 'claude_agent_sdk'
require 'async'
Async do
client = ClaudeAgentSDK::Client.new
begin
client.connect
client.query("What is the capital of France?")
client.receive_response { |msg| puts msg }
ensure
client.disconnect
end
end.waitAdvanced features (interrupt, mid-session model/permission switching, MCP status, custom transports for E2B/SSH/etc.) → see docs/client.md.
Define tools as Ruby procs/lambdas that run in-process — no subprocess, no IPC, direct access to your app state.
greet = ClaudeAgentSDK.create_tool('greet', 'Greet a user', { name: :string }) do |args|
{ content: [{ type: 'text', text: "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']
)Tool arguments are JSON-Schema-validated (draft4, via the official mcp gem) before your handler runs: the simple { name: :string } idiom marks every parameter required, so a missing argument returns an in-band error to the model instead of invoking the handler with nil. Handler exceptions and unknown tools are also reported in-band (isError: true) so the model can read the text and self-correct. Opt out globally with MCP.configure { |c| c.validate_tool_call_arguments = false }. Schemas the draft4 metaschema rejects (e.g. numeric exclusiveMinimum, $ref) fall back to validation-disabled with a warning.
Resources, prompts, mixed (SDK + external) servers, RubyLLM schema compatibility → see docs/mcp-servers.md.
Hooks let the Claude Code application invoke your Ruby code at all 27 lifecycle points (PreToolUse, PostToolUse, UserPromptSubmit, Stop, PreCompact, etc.) with typed input objects. Permission callbacks give you programmatic control over tool execution.
options = ClaudeAgentSDK::ClaudeAgentOptions.new(
hooks: { 'PreToolUse' => [ClaudeAgentSDK::HookMatcher.new(matcher: 'Bash', hooks: [my_hook])] },
can_use_tool: my_permission_callback
)→ Full event list, typed inputs, and worked examples in docs/hooks-and-permissions.md.
| Topic | Reference |
|---|---|
| Structured output, thinking config, budget, fallback model, advisor model, beta features, sandbox, bare mode, file checkpointing | docs/configuration.md |
| Session listing, reading, renaming, tagging, deleting, forking, resume-at-message | docs/sessions.md |
| OpenTelemetry tracing, Langfuse setup, custom observers | docs/observability.md |
Rails integration (fiber safety, solid_queue fiber workers / callback_scheduling: :inline, ActionCable, sessions, jobs, HTTP MCP, observability initializer) |
docs/rails.md |
| Message, content block, and configuration type reference | docs/types.md |
| Error handling, exception hierarchy, timeout configuration | docs/errors.md |
| Example | Description |
|---|---|
| quick_start.rb | Basic query() usage with options |
| client_example.rb | Interactive Client usage |
| message_types_example.rb | Handling all 24 SDK message types |
| streaming_input_example.rb | Streaming input for multi-turn conversations |
| session_resumption_example.rb | Multi-turn conversations with session persistence |
| structured_output_example.rb | JSON schema structured output |
| error_handling_example.rb | Error handling with AssistantMessage.error |
| bare_mode_example.rb | Minimal startup with bare: true |
| sandbox_example.rb | Full sandbox settings (network, filesystem, violations) |
| Example | Description |
|---|---|
| mcp_calculator.rb | Custom tools with SDK MCP servers |
| mcp_resources_prompts_example.rb | MCP resources and prompts |
| http_mcp_server_example.rb | HTTP/SSE MCP server configuration |
| Example | Description |
|---|---|
| hooks_example.rb | Using hooks to control tool execution |
| advanced_hooks_example.rb | Typed hook inputs/outputs |
| lifecycle_hooks_example.rb | All 27 hook events |
| permission_callback_example.rb | Dynamic tool permission control |
| Example | Description |
|---|---|
| budget_control_example.rb | Budget control with max_budget_usd |
| fallback_model_example.rb | Fallback model configuration |
| advisor_example.rb | Server-side advisor tool (advisor_model) |
| extended_thinking_example.rb | Extended thinking |
| e2b_transport_example.rb | Custom transport running CLI in an E2B microVM |
| Example | Description |
|---|---|
| otel_langfuse_example.rb | OpenTelemetry tracing with Langfuse backend |
| rails_actioncable_example.rb | ActionCable streaming to frontend |
| rails_background_job_example.rb | Background jobs with session resumption |
See the Claude Code documentation for a complete list of available tools.
After checking out the repo, run bundle install to install dependencies. Then bundle exec rspec to run the tests.
The gem is available as open source under the terms of the MIT License.
