This guide walks you through the ClaudeAgent Ruby SDK from first install to multi-turn conversations. Each section builds on the last, starting with the simplest API.
- Ruby 3.2+
- Claude Code CLI v2.0.0+ (install guide)
Verify both are available:
ruby -v # >= 3.2.0
claude -v # >= 2.0.0Add to your Gemfile:
gem "claude_agent"Then:
bundle installOr install directly:
gem install claude_agentThe fastest way to get a response is ClaudeAgent.ask. It sends a single prompt and returns a TurnResult:
require "claude_agent"
turn = ClaudeAgent.ask("What is the capital of France?")
puts turn.text
# => "The capital of France is Paris."TurnResult gives you structured access to everything that happened during the turn:
turn = ClaudeAgent.ask("Explain Ruby's GIL in one sentence.")
puts turn.text # Combined text from all assistant messages
puts turn.cost # Total cost in USD (e.g., 0.003)
puts turn.duration_ms # Wall-clock time in milliseconds
puts turn.session_id # Session ID (for resuming later)
puts turn.model # Model that handled the request
puts turn.tool_uses.size # Number of tools Claude invoked
puts turn.success? # true if no errorsOverride defaults with keyword arguments:
turn = ClaudeAgent.ask("Fix the bug in auth.rb",
model: "opus",
max_turns: 5,
permission_mode: "acceptEdits"
)Pass a block to ask to receive each message as it arrives:
turn = ClaudeAgent.ask("Explain how TCP works") do |message|
case message
when ClaudeAgent::AssistantMessage
print message.text
when ClaudeAgent::ResultMessage
puts "\n--- Done (cost: $#{message.total_cost_usd}) ---"
end
endThe block receives every message in the protocol stream. The return value is still a TurnResult with the full accumulated data.
For cleaner streaming, use on_stream to receive just the text:
turn = ClaudeAgent.ask("Write a haiku about Ruby",
on_stream: ->(text) { print text }
)
puts "\nCost: $#{turn.cost}"Use ClaudeAgent.chat for back-and-forth conversations. The block form auto-closes the connection when done:
ClaudeAgent.chat do |c|
c.say("What files are in the current directory?")
c.say("Which one is the largest?")
c.say("Show me the first 10 lines of that file.")
puts "Total cost: $#{c.total_cost}"
endEach call to say returns a TurnResult:
ClaudeAgent.chat(model: "opus") do |c|
turn = c.say("Write a function that reverses a string")
puts turn.text
turn = c.say("Now add error handling")
puts turn.text
puts "Tools used: #{turn.tool_uses.map(&:name).join(", ")}"
endIf you need the conversation to outlive a block, skip it:
conversation = ClaudeAgent.chat(model: "sonnet")
conversation.say("Hello")
conversation.say("Tell me more")
conversation.close # Always close when donePass on_stream to print text as it arrives:
ClaudeAgent.chat(on_stream: ->(text) { print text }) do |c|
c.say("What is 2+2?")
puts # newline after streamed output
c.say("Now multiply that by 10")
puts
endOr stream per-turn with a block on say:
ClaudeAgent.chat do |c|
c.say("Explain monads") do |message|
print message.text if message.is_a?(ClaudeAgent::AssistantMessage)
end
endSet defaults that apply to every ask and chat call:
ClaudeAgent.model = "opus"
ClaudeAgent.max_turns = 10
ClaudeAgent.permission_mode = "acceptEdits"
ClaudeAgent.system_prompt = "You are a helpful coding assistant."Or configure in bulk:
ClaudeAgent.configure do |c|
c.model = "opus"
c.max_turns = 10
c.permission_mode = "acceptEdits"
c.system_prompt = "You are a helpful coding assistant."
c.max_budget_usd = 1.0
c.cwd = "/path/to/project"
endPer-request keyword arguments always override global defaults:
ClaudeAgent.model = "sonnet"
# This request uses opus despite the global default
turn = ClaudeAgent.ask("Complex question", model: "opus")Reset to defaults with:
ClaudeAgent.reset_config!See Configuration for the full list of options.
For full control, use ClaudeAgent::Conversation directly. It supports callbacks, permission handling, tool tracking, and session management.
conversation = ClaudeAgent::Conversation.open(
model: "opus",
max_turns: 10,
permission_mode: "acceptEdits",
on_stream: ->(text) { print text },
on_tool_use: ->(tool) { puts "\nUsing tool: #{tool.name}" },
on_result: ->(result) { puts "\nTurn cost: $#{result.total_cost_usd}" }
) do |c|
c.say("Read the file config.rb and explain what it does")
c.say("Refactor it to use keyword arguments")
puts "Session: #{c.session_id}"
puts "Total cost: $#{c.total_cost}"
puts "Turns: #{c.turns.size}"
end| Callback | Receives | When |
|---|---|---|
on_stream |
String |
Each text chunk as it streams |
on_text |
String |
Full text from each assistant message |
on_thinking |
String |
Thinking/reasoning content |
on_tool_use |
ToolUseBlock |
Claude requests a tool |
on_tool_result |
ToolResultBlock, ToolUseBlock |
Tool result arrives (paired with request) |
on_result |
ResultMessage |
Turn completes |
on_message |
Message |
Every message (catch-all) |
By default, Conversation queues permission requests for you to handle. You can also set a mode or provide a custom callback:
# Use a CLI permission mode
ClaudeAgent::Conversation.open(on_permission: :accept_edits) do |c|
c.say("Fix the typo in README.md")
end
# Or provide a lambda
ClaudeAgent::Conversation.open(
on_permission: ->(tool_name, input, ctx) {
ClaudeAgent::PermissionResultAllow.new
}
) do |c|
c.say("Update the config file")
endFor declarative permission rules, see Permissions.
Every turn returns a session_id. Save it to resume the conversation later:
# First session
session_id = nil
ClaudeAgent.chat do |c|
c.say("Let's work on the authentication module")
session_id = c.session_id
end
# Later — resume where you left off
ClaudeAgent.resume_conversation(session_id) do |c|
c.say("Continue with the tests we discussed")
endresume_conversation returns a Conversation, so it supports the same block form and callbacks.
You can also resume via Conversation.resume directly:
conversation = ClaudeAgent::Conversation.resume(session_id,
on_stream: ->(text) { print text }
)
conversation.say("Pick up where we left off")
conversation.closeBrowse previous sessions without spawning a CLI process:
sessions = ClaudeAgent.list_sessions(dir: "/path/to/project", limit: 10)
sessions.each do |session|
puts "#{session.session_id}: #{session.title} (#{session.updated_at})"
end- Configuration -- full list of options, permission modes, and sandbox settings
- Conversations -- advanced multi-turn patterns, tool tracking, and abort handling
- Permissions -- declarative permission policies and the
can_use_toolcallback - Hooks -- lifecycle hooks for tool use, session events, and more
- Messages -- all message types, content blocks, and the event system
- MCP Servers -- integrating external tools via Model Context Protocol
- Sessions -- listing, reading, forking, and managing past sessions