Claude Agent SDK for Ruby
Disclaimer: This is an unofficial, community-maintained Ruby SDK for Claude Agent. It is not officially supported by Anthropic. For official SDK support, see the Python SDK.
This implementation is based on the official Python SDK and aims to provide feature parity for Ruby developers. Use at your own risk.
Table of Contents
- Installation
- Quick Start
- Basic Usage: query()
- Client
- Custom Tools (SDK MCP Servers)
- Hooks
- Permission Callbacks
- Structured Output
- Budget Control
- Fallback Model
- Types
- Error Handling
- Examples
- Development
- License
Installation
Add this line to your application's Gemfile:
gem 'claude-agent-sdk', '~> 0.3.0'And then execute:
bundle installOr install it yourself as:
gem install claude-agent-sdkPrerequisites:
- Ruby 3.2+
- Node.js
- Claude Code 2.0.0+:
npm install -g @anthropic-ai/claude-code
Quick Start
require 'claude_agent_sdk'
ClaudeAgentSDK.query(prompt: "What is 2 + 2?") do |message|
puts message
endBasic Usage: query()
query() 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|
if message.is_a?(ClaudeAgentSDK::AssistantMessage)
message.content.each do |block|
puts block.text if block.is_a?(ClaudeAgentSDK::TextBlock)
end
end
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' # auto-accept file edits
)
ClaudeAgentSDK.query(
prompt: "Create a hello.rb file",
options: options
) do |message|
# Process tool use and results
endWorking Directory
options = ClaudeAgentSDK::ClaudeAgentOptions.new(
cwd: "/path/to/project"
)Streaming Input
The query() function supports streaming input, allowing you to send multiple messages dynamically instead of a single prompt string.
require 'claude_agent_sdk'
# Create a stream of messages
messages = ['Hello!', 'What is 2+2?', 'Thanks!']
stream = ClaudeAgentSDK::Streaming.from_array(messages)
# Query with streaming input
ClaudeAgentSDK.query(prompt: stream) do |message|
puts message if message.is_a?(ClaudeAgentSDK::AssistantMessage)
endYou can also create custom streaming enumerators:
# Dynamic message generation
stream = Enumerator.new do |yielder|
yielder << ClaudeAgentSDK::Streaming.user_message("First message")
# Do some processing...
yielder << ClaudeAgentSDK::Streaming.user_message("Second message")
yielder << ClaudeAgentSDK::Streaming.user_message("Third message")
end
ClaudeAgentSDK.query(prompt: stream) do |message|
# Process responses
endFor a complete example, see examples/streaming_input_example.rb.
Client
ClaudeAgentSDK::Client supports bidirectional, interactive conversations with Claude Code. Unlike query(), Client enables custom tools, hooks, and permission callbacks, all of which can be defined as Ruby procs/lambdas.
The Client class automatically uses streaming mode for bidirectional communication, allowing you to send multiple queries dynamically during a single session without closing the connection.
Basic Client Usage
require 'claude_agent_sdk'
require 'async'
Async do
client = ClaudeAgentSDK::Client.new
begin
# Connect automatically uses streaming mode for bidirectional communication
client.connect
# Send a query
client.query("What is the capital of France?")
# Receive the response
client.receive_response do |msg|
if msg.is_a?(ClaudeAgentSDK::AssistantMessage)
msg.content.each do |block|
puts block.text if block.is_a?(ClaudeAgentSDK::TextBlock)
end
elsif msg.is_a?(ClaudeAgentSDK::ResultMessage)
puts "Cost: $#{msg.total_cost_usd}" if msg.total_cost_usd
end
end
ensure
client.disconnect
end
end.waitAdvanced Client Features
Async do
client = ClaudeAgentSDK::Client.new
client.connect
# Send interrupt signal
client.interrupt
# Change permission mode during conversation
client.set_permission_mode('acceptEdits')
# Change AI model during conversation
client.set_model('claude-sonnet-4-5')
# Get server initialization info
info = client.server_info
puts "Available commands: #{info}"
client.disconnect
end.waitCustom Tools (SDK MCP Servers)
A custom tool is a Ruby proc/lambda that you can offer to Claude, for Claude to invoke as needed.
Custom tools are implemented as in-process MCP servers that run directly within your Ruby application, eliminating the need for separate processes that regular MCP servers require.
Implementation: This SDK uses the official Ruby MCP SDK (mcp gem) internally, providing full protocol compliance while offering a simpler block-based API for tool definition.
Creating a Simple Tool
require 'claude_agent_sdk'
require 'async'
# Define a tool using create_tool
greet_tool = ClaudeAgentSDK.create_tool('greet', 'Greet a user', { name: :string }) do |args|
{ content: [{ type: 'text', text: "Hello, #{args[:name]}!" }] }
end
# Create an SDK MCP server
server = ClaudeAgentSDK.create_sdk_mcp_server(
name: 'my-tools',
version: '1.0.0',
tools: [greet_tool]
)
# Use it with Claude
options = ClaudeAgentSDK::ClaudeAgentOptions.new(
mcp_servers: { tools: server },
allowed_tools: ['mcp__tools__greet']
)
Async do
client = ClaudeAgentSDK::Client.new(options: options)
client.connect
client.query("Greet Alice")
client.receive_response { |msg| puts msg }
client.disconnect
end.waitBenefits Over External MCP Servers
- No subprocess management - Runs in the same process as your application
- Better performance - No IPC overhead for tool calls
- Simpler deployment - Single Ruby process instead of multiple
- Easier debugging - All code runs in the same process
- Direct access - Tools can directly access your application's state
Calculator Example
# Define calculator tools
add_tool = ClaudeAgentSDK.create_tool('add', 'Add two numbers', { a: :number, b: :number }) do |args|
result = args[:a] + args[:b]
{ content: [{ type: 'text', text: "#{args[:a]} + #{args[:b]} = #{result}" }] }
end
divide_tool = ClaudeAgentSDK.create_tool('divide', 'Divide numbers', { a: :number, b: :number }) do |args|
if args[:b] == 0
{ content: [{ type: 'text', text: 'Error: Division by zero' }], is_error: true }
else
result = args[:a] / args[:b]
{ content: [{ type: 'text', text: "Result: #{result}" }] }
end
end
# Create server
calculator = ClaudeAgentSDK.create_sdk_mcp_server(
name: 'calculator',
tools: [add_tool, divide_tool]
)
options = ClaudeAgentSDK::ClaudeAgentOptions.new(
mcp_servers: { calc: calculator },
allowed_tools: ['mcp__calc__add', 'mcp__calc__divide']
)Mixed Server Support
You can use both SDK and external MCP servers together:
options = ClaudeAgentSDK::ClaudeAgentOptions.new(
mcp_servers: {
internal: sdk_server, # In-process SDK server
external: { # External subprocess server
type: 'stdio',
command: 'external-server'
}
}
)MCP Resources and Prompts
SDK MCP servers can also expose resources (data sources) and prompts (reusable templates):
# Create a resource (data source Claude can read)
config_resource = ClaudeAgentSDK.create_resource(
uri: 'config://app/settings',
name: 'Application Settings',
description: 'Current app configuration',
mime_type: 'application/json'
) do
config_data = { app_name: 'MyApp', version: '1.0.0' }
{
contents: [{
uri: 'config://app/settings',
mimeType: 'application/json',
text: JSON.pretty_generate(config_data)
}]
}
end
# Create a prompt template
review_prompt = ClaudeAgentSDK.create_prompt(
name: 'code_review',
description: 'Review code for best practices',
arguments: [
{ name: 'code', description: 'Code to review', required: true }
]
) do |args|
{
messages: [{
role: 'user',
content: {
type: 'text',
text: "Review this code: #{args[:code]}"
}
}]
}
end
# Create server with tools, resources, and prompts
server = ClaudeAgentSDK.create_sdk_mcp_server(
name: 'dev-tools',
tools: [my_tool],
resources: [config_resource],
prompts: [review_prompt]
)For complete examples, see examples/mcp_calculator.rb and examples/mcp_resources_prompts_example.rb.
Hooks
A hook is a Ruby proc/lambda that the Claude Code application (not Claude) invokes at specific points of the Claude agent loop. Hooks can provide deterministic processing and automated feedback for Claude. Read more in Claude Code Hooks Reference.
Example
require 'claude_agent_sdk'
require 'async'
Async do
# Define a hook that blocks dangerous bash commands
bash_hook = lambda do |input, tool_use_id, context|
tool_name = input[:tool_name]
tool_input = input[:tool_input]
return {} unless tool_name == 'Bash'
command = tool_input[:command] || ''
block_patterns = ['rm -rf', 'foo.sh']
block_patterns.each do |pattern|
if command.include?(pattern)
return {
hookSpecificOutput: {
hookEventName: 'PreToolUse',
permissionDecision: 'deny',
permissionDecisionReason: "Command contains forbidden pattern: #{pattern}"
}
}
end
end
{} # Allow if no patterns match
end
# Create options with hook
options = ClaudeAgentSDK::ClaudeAgentOptions.new(
allowed_tools: ['Bash'],
hooks: {
'PreToolUse' => [
ClaudeAgentSDK::HookMatcher.new(
matcher: 'Bash',
hooks: [bash_hook]
)
]
}
)
client = ClaudeAgentSDK::Client.new(options: options)
client.connect
# Test: Command with forbidden pattern (will be blocked)
client.query("Run the bash command: ./foo.sh --help")
client.receive_response { |msg| puts msg }
client.disconnect
end.waitFor more examples, see examples/hooks_example.rb.
Permission Callbacks
A permission callback is a Ruby proc/lambda that allows you to programmatically control tool execution. This gives you fine-grained control over what tools Claude can use and with what inputs.
Example
require 'claude_agent_sdk'
require 'async'
Async do
# Define a permission callback
permission_callback = lambda do |tool_name, input, context|
# Allow Read operations
if tool_name == 'Read'
return ClaudeAgentSDK::PermissionResultAllow.new
end
# Block Write to sensitive files
if tool_name == 'Write'
file_path = input[:file_path] || input['file_path']
if file_path && file_path.include?('/etc/')
return ClaudeAgentSDK::PermissionResultDeny.new(
message: 'Cannot write to sensitive system files',
interrupt: false
)
end
return ClaudeAgentSDK::PermissionResultAllow.new
end
# Default: allow
ClaudeAgentSDK::PermissionResultAllow.new
end
# Create options with permission callback
options = ClaudeAgentSDK::ClaudeAgentOptions.new(
allowed_tools: ['Read', 'Write', 'Bash'],
can_use_tool: permission_callback
)
client = ClaudeAgentSDK::Client.new(options: options)
client.connect
# This will be allowed
client.query("Create a file called test.txt with content 'Hello'")
client.receive_response { |msg| puts msg }
# This will be blocked
client.query("Write to /etc/passwd")
client.receive_response { |msg| puts msg }
client.disconnect
end.waitFor more examples, see examples/permission_callback_example.rb.
Structured Output
Use output_format to get validated JSON responses matching a schema. The Claude CLI returns structured output via a StructuredOutput tool use block.
require 'claude_agent_sdk'
require 'json'
# Define a JSON schema
schema = {
type: 'object',
properties: {
name: { type: 'string' },
age: { type: 'integer' },
skills: { type: 'array', items: { type: 'string' } }
},
required: %w[name age skills]
}
options = ClaudeAgentSDK::ClaudeAgentOptions.new(
output_format: { type: 'json_schema', schema: schema },
max_turns: 3
)
structured_data = nil
ClaudeAgentSDK.query(
prompt: "Create a profile for a software engineer",
options: options
) do |message|
if message.is_a?(ClaudeAgentSDK::AssistantMessage)
message.content.each do |block|
# Structured output comes via StructuredOutput tool use
if block.is_a?(ClaudeAgentSDK::ToolUseBlock) && block.name == 'StructuredOutput'
structured_data = block.input
end
end
end
end
if structured_data
puts "Name: #{structured_data[:name]}"
puts "Age: #{structured_data[:age]}"
puts "Skills: #{structured_data[:skills].join(', ')}"
endFor complete examples, see examples/structured_output_example.rb.
Budget Control
Use max_budget_usd to set a spending cap for your queries:
options = ClaudeAgentSDK::ClaudeAgentOptions.new(
max_budget_usd: 0.10, # Cap at $0.10
max_turns: 3
)
ClaudeAgentSDK.query(prompt: "Explain recursion", options: options) do |message|
if message.is_a?(ClaudeAgentSDK::ResultMessage)
puts "Cost: $#{message.total_cost_usd}"
end
endFor complete examples, see examples/budget_control_example.rb.
Fallback Model
Use fallback_model to specify a backup model if the primary is unavailable:
options = ClaudeAgentSDK::ClaudeAgentOptions.new(
model: 'claude-sonnet-4-20250514',
fallback_model: 'claude-3-5-haiku-20241022'
)
ClaudeAgentSDK.query(prompt: "Hello", options: options) do |message|
if message.is_a?(ClaudeAgentSDK::AssistantMessage)
puts "Model used: #{message.model}"
end
endFor complete examples, see examples/fallback_model_example.rb.
Types
See lib/claude_agent_sdk/types.rb for complete type definitions.
Message Types
# Union type of all possible messages
Message = UserMessage | AssistantMessage | SystemMessage | ResultMessageUserMessage
User input message.
class UserMessage
attr_accessor :content, # String | Array<ContentBlock>
:parent_tool_use_id # String | nil
endAssistantMessage
Assistant response message with content blocks.
class AssistantMessage
attr_accessor :content, # Array<ContentBlock>
:model, # String
:parent_tool_use_id,# String | nil
:error # String | nil ('authentication_failed', 'billing_error', 'rate_limit', 'invalid_request', 'server_error', 'unknown')
endSystemMessage
System message with metadata.
class SystemMessage
attr_accessor :subtype, # String ('init', etc.)
:data # Hash
endResultMessage
Final result message with cost and usage information.
class ResultMessage
attr_accessor :subtype, # String
:duration_ms, # Integer
:duration_api_ms, # Integer
:is_error, # Boolean
:num_turns, # Integer
:session_id, # String
:total_cost_usd, # Float | nil
:usage, # Hash | nil
:result, # String | nil (final text result)
:structured_output # Hash | nil (when using output_format)
endContent Block Types
# Union type of all content blocks
ContentBlock = TextBlock | ThinkingBlock | ToolUseBlock | ToolResultBlockTextBlock
Text content block.
class TextBlock
attr_accessor :text # String
endThinkingBlock
Thinking content block (for models with extended thinking capability).
class ThinkingBlock
attr_accessor :thinking, # String
:signature # String
endToolUseBlock
Tool use request block.
class ToolUseBlock
attr_accessor :id, # String
:name, # String
:input # Hash
endToolResultBlock
Tool execution result block.
class ToolResultBlock
attr_accessor :tool_use_id, # String
:content, # String | Array<Hash> | nil
:is_error # Boolean | nil
endError Types
# Base exception class for all SDK errors
class ClaudeSDKError < StandardError; end
# Raised when Claude Code CLI is not found
class CLINotFoundError < CLIConnectionError
# @param message [String] Error message (default: "Claude Code not found")
# @param cli_path [String, nil] Optional path to the CLI that was not found
end
# Raised when connection to Claude Code fails
class CLIConnectionError < ClaudeSDKError; end
# Raised when the Claude Code process fails
class ProcessError < ClaudeSDKError
attr_reader :exit_code, # Integer | nil
:stderr # String | nil
end
# Raised when JSON parsing fails
class CLIJSONDecodeError < ClaudeSDKError
attr_reader :line, # String - The line that failed to parse
:original_error # Exception - The original JSON decode exception
end
# Raised when message parsing fails
class MessageParseError < ClaudeSDKError
attr_reader :data # Hash | nil
endConfiguration Types
| Type | Description |
|---|---|
ClaudeAgentOptions |
Main configuration for queries and clients |
HookMatcher |
Hook configuration with matcher pattern and timeout |
PermissionResultAllow |
Permission callback result to allow tool use |
PermissionResultDeny |
Permission callback result to deny tool use |
AgentDefinition |
Agent definition with description, prompt, tools, model |
McpStdioServerConfig |
MCP server config for stdio transport |
McpSSEServerConfig |
MCP server config for SSE transport |
McpHttpServerConfig |
MCP server config for HTTP transport |
SdkPluginConfig |
SDK plugin configuration |
Error Handling
AssistantMessage Errors
AssistantMessage includes an error field for API-level errors:
ClaudeAgentSDK.query(prompt: "Hello") do |message|
if message.is_a?(ClaudeAgentSDK::AssistantMessage) && message.error
case message.error
when 'rate_limit'
puts "Rate limited - retry after delay"
when 'authentication_failed'
puts "Check your API key"
when 'billing_error'
puts "Check your billing status"
when 'invalid_request'
puts "Invalid request format"
when 'server_error'
puts "Server error - retry later"
end
end
endFor complete examples, see examples/error_handling_example.rb.
Exception Handling
require 'claude_agent_sdk'
begin
ClaudeAgentSDK.query(prompt: "Hello") do |message|
puts message
end
rescue ClaudeAgentSDK::CLINotFoundError
puts "Please install Claude Code"
rescue ClaudeAgentSDK::ProcessError => e
puts "Process failed with exit code: #{e.exit_code}"
rescue ClaudeAgentSDK::CLIJSONDecodeError => e
puts "Failed to parse response: #{e}"
endError Types
| Error | Description |
|---|---|
ClaudeSDKError |
Base error for all SDK errors |
CLINotFoundError |
Claude Code not installed |
CLIConnectionError |
Connection issues |
ProcessError |
Process failed (includes exit_code and stderr) |
CLIJSONDecodeError |
JSON parsing issues |
MessageParseError |
Message parsing issues |
See lib/claude_agent_sdk/errors.rb for all error types.
Available Tools
See the Claude Code documentation for a complete list of available tools.
Examples
| Example | Description |
|---|---|
| examples/quick_start.rb | Basic query() usage with options |
| examples/client_example.rb | Interactive Client usage |
| examples/streaming_input_example.rb | Streaming input for multi-turn conversations |
| examples/mcp_calculator.rb | Custom tools with SDK MCP servers |
| examples/mcp_resources_prompts_example.rb | MCP resources and prompts |
| examples/hooks_example.rb | Using hooks to control tool execution |
| examples/permission_callback_example.rb | Dynamic tool permission control |
| examples/structured_output_example.rb | JSON schema structured output |
| examples/budget_control_example.rb | Budget control with max_budget_usd
|
| examples/fallback_model_example.rb | Fallback model configuration |
| examples/advanced_hooks_example.rb | Typed hook inputs/outputs |
| examples/error_handling_example.rb | Error handling with AssistantMessage.error
|
| examples/extended_thinking_example.rb | Extended thinking (API parity) |
Development
After checking out the repo, run bundle install to install dependencies. Then, run bundle exec rspec to run the tests.
License
The gem is available as open source under the terms of the MIT License.