claude-agent-cr v0.6.0
Claude Agent SDK for Crystal
An unofficial Anthropic Agent SDK for Crystal, enabling developers to build autonomous AI agents powered by Claude and the Claude Code CLI.
This library provides a programmatic interface to the Claude Code CLI, allowing you to create agents that can execute commands, edit files, and perform complex multi-step workflows.
Note: A large portion of this library was written with the assistance of AI (Claude), including code, tests, and documentation.
Features
- One-Shot Queries: Simple interface for single-turn agent tasks.
- Interactive Sessions: Full bidirectional agent control for chat applications.
- V2 Streaming Interface: Send/receive patterns for real-time communication.
- Tool Use: Access to Claude's built-in tools (Bash, File Edit, etc.) and support for custom tools.
- SDK MCP Servers: In-process MCP servers with custom tools via control protocol (same architecture as official SDKs).
- External MCP Servers: Connect to external MCP servers (stdio, HTTP, SSE transports).
- Runtime MCP Management:
set_mcp_servers,enable_mcp_channel, andreload_pluginsto mutate the session without restarting the subprocess. - Preset Types: Type-safe presets for system prompts and tools to prevent typos.
- System Prompt Options: Inline strings,
SystemPromptPreset(withexclude_dynamic_sectionsfor cache-friendly prompts), orSystemPromptFileloaded from disk. - Type-Safe Schemas: Crystal's answer to Zod - generate JSON schemas from types.
- Structured Outputs: Get validated JSON responses matching your schema.
- Dynamic Controls: Change model and permission mode live, inspect MCP status, stop tasks,
get_context_usage,prompt_suggestion. - Skills: Enable SDK-managed skill access (
"all"or a named list) with automaticsetting_sourcesdefaults. - Task Budgets: API-side token budget awareness via
task_budgetso the model paces tool use. - Server Info: Access CLI initialization metadata like commands, models, and output styles.
- Subagents: Define specialized agents with rich fields (
disallowed_tools,memory,initial_prompt,max_turns,background,effort,permission_mode, per-agentmcp_servers). - Hooks & Permissions: Granular control over what the agent is allowed to do with full hook support (PreToolUse, PostToolUse, PreCompact, Notification, TeammateIdle, TaskCompleted, ConfigChange, etc.).
- Permission Modes:
default,acceptEdits,plan,bypassPermissions,auto,dontAsk. - Session Management: Resume, fork, delete, and continue conversations with precise message-level resume.
- Session History & Subagents: List saved sessions, fork a session branch (
fork_session), list subagents (list_subagents), read subagent transcripts (get_subagent_messages), delete sessions (delete_session). - Transcript Controls:
should_query: falseto append user messages without triggering a turn;include_system_messagesonget_session_messages. - File Checkpointing: Track and rewind file changes.
- Sandbox Support: Configure sandboxed execution environments.
- Extended Thinking:
ThinkingConfig.adaptive/enabled/disabled, plus fine-grainedefforttiers (Low,Medium,High,Xhigh,Max) —xhighis the default Claude Code tier for Opus 4.7. - Pre-warming:
ClaudeAgent.startupspins up the CLI subprocess ahead of the first query. - Tracing: Automatic W3C trace context propagation (
TRACEPARENT/TRACESTATE) when present in the caller's environment. - Streaming: Real-time message streaming using Crystal's native fibers.
- Type Safety: Fully typed message and event structures, including new
ApiRetryMessage,MemoryRecallMessage,StatusMessage, andMirrorErrorMessage, with graceful handling of unknown content types.
Prerequisites
- Crystal: >= 1.10.0
- Claude Code CLI: You must have the
claudeCLI installed and authenticated.- Install via curl:
curl -fsSL https://claude.ai/install.sh | bash - Authenticate:
claude login
- Install via curl:
Installation
-
Add the dependency to your
shard.yml:dependencies: claude-agent-cr: github: amscotti/claude-agent-cr version: ~> 0.6.0 -
Run
shards install
Usage
Basic Query
For simple, one-off tasks where you want the agent to do something and return the result.
require "claude-agent-cr"
begin
ClaudeAgent.query("Create a file named hello.txt with 'Hello World'") do |message|
# Stream the agent's thought process and output
if message.is_a?(ClaudeAgent::AssistantMessage)
print message.text
end
end
rescue ex : ClaudeAgent::CLINotFoundError
puts "Please install Claude CLI"
end
Interactive Conversation
For building chatbots or interactive agent sessions.
require "claude-agent-cr"
ClaudeAgent::AgentClient.open do |client|
# Initial query
client.query("Check the current directory status")
# Process responses
client.each_response do |message|
case message
when ClaudeAgent::AssistantMessage
puts "Claude: #{message.text}"
when ClaudeAgent::PermissionRequest
# Automatically approve or handle via callback
puts "Agent wants to use #{message.tool_name}..."
end
end
# Follow up
client.send_user_message("Now create a summary file.")
client.each_response do |message|
# ... handle responses
end
end
Dynamic Controls
Change live session settings while an AgentClient is connected.
ClaudeAgent::AgentClient.open do |client|
client.query("Review this repository")
client.each_response { |message| puts message }
client.set_permission_mode(ClaudeAgent::PermissionMode::Plan)
client.set_model("claude-sonnet-4-5")
status = client.get_mcp_status
puts "Configured MCP servers: #{status.mcp_servers.size}"
# Apply flag settings, generate a title, toggle proactive mode
client.apply_flag_settings({"verbose" => JSON::Any.new(true)})
title = client.generate_session_title("Repository review session", persist: true)
puts "Generated title: #{title}"
client.set_proactive(true)
client.enable_remote_control(true)
end
Rich Event Messages
Streaming clients can react to typed init, task, rate-limit, prompt-suggestion, elicitation-complete, and fallback event messages.
ClaudeAgent::AgentClient.open do |client|
client.query("Inspect this repository and suggest one test improvement")
client.each_response do |message|
case message
when ClaudeAgent::InitMessage
puts "Output style: #{message.output_style}"
puts "Slash commands: #{message.slash_commands.join(", ")}"
when ClaudeAgent::TaskStartedMessage
puts "Task started: #{message.description}"
when ClaudeAgent::TaskProgressMessage
puts "Task tokens: #{message.usage.total_tokens}"
when ClaudeAgent::TaskNotificationMessage
puts "Task status: #{message.status}"
when ClaudeAgent::RateLimitEvent
puts "Rate limit status: #{message.rate_limit_info.status}"
when ClaudeAgent::ElicitationCompleteMessage
puts "Elicitation complete for #{message.mcp_server_name}"
when ClaudeAgent::PromptSuggestionMessage
puts "Suggestion: #{message.suggestion}"
when ClaudeAgent::UnknownMessage
puts "Unknown event type: #{message.type}"
end
end
end
Server Info
Inspect the Claude CLI initialization metadata for available commands and output styles. get_server_info returns a typed ClaudeAgent::ServerInfo wrapper. AgentClient#supported_agents and AgentClient#supported_commands provide small convenience helpers over that metadata.
ClaudeAgent::AgentClient.open do |client|
if info = client.get_server_info
puts "Commands available: #{info.commands.size}"
puts "Output style: #{info.output_style || "default"}"
puts "Supported agents: #{client.supported_agents.map(&.name).join(", ")}"
end
end
Settings And Async Message Controls
Inspect effective settings, receive prompt suggestions, and cancel a queued user message.
ClaudeAgent::AgentClient.open(ClaudeAgent::AgentOptions.new(prompt_suggestions: true)) do |client|
settings = client.settings
applied = settings["applied"]?.try(&.as_h?)
puts "Model: #{applied.try(&.["model"]?.try(&.as_s?)) || "(unknown)"}"
queued_uuid = UUID.random.to_s
client.query("Answer with exactly: FIRST")
client.send_user_message("Queued follow-up", uuid: queued_uuid)
puts "Cancelled: #{client.cancel_async_message(queued_uuid)}"
end
MCP Elicitation
Handle MCP user-input requests programmatically when a server requires form or URL-based auth.
hooks = ClaudeAgent::HookConfig.new(
elicitation: [->(input : ClaudeAgent::HookInput, _id : String, _ctx : ClaudeAgent::HookContext) {
puts "Elicitation from #{input.mcp_server_name}: #{input.elicitation_message}"
ClaudeAgent::HookResult.elicitation("decline")
}],
)
options = ClaudeAgent::AgentOptions.new(
hooks: hooks,
on_elicitation: ->(request : ClaudeAgent::ElicitationRequest) {
request.mode == "url" ? ClaudeAgent::ElicitationResponse.decline : ClaudeAgent::ElicitationResponse.accept
},
)
Hook And Permission Overrides
Hooks and permission callbacks can now return runtime modifications that the CLI honors.
rewrite_bash = ->(input : ClaudeAgent::HookInput, _id : String, _ctx : ClaudeAgent::HookContext) {
if input.tool_name == "Bash"
ClaudeAgent::HookResult.allow_with_input({
"command" => JSON::Any.new("pwd"),
})
else
ClaudeAgent::HookResult.allow
end
}
hooks = ClaudeAgent::HookConfig.new(
pre_tool_use: [ClaudeAgent::HookMatcher.new(matcher: "Bash", hooks: [rewrite_bash])],
)
ClaudeAgent::AgentClient.open(ClaudeAgent::AgentOptions.new(hooks: hooks)) do |client|
client.query("Use Bash to run ls -la")
client.each_response { |message| puts message }
end
Configuration
You can customize the agent's behavior using AgentOptions.
options = ClaudeAgent::AgentOptions.new(
model: "claude-opus-4-7",
max_turns: 10,
# Restrict tools
allowed_tools: ["Read", "LS"],
# Handle permissions automatically
permission_mode: ClaudeAgent::PermissionMode::Default,
# System prompt
system_prompt: "You are a coding assistant."
)
ClaudeAgent.query("List files", options) { |msg| puts msg }
Preset Types
Use type-safe presets instead of strings to prevent typos and get IDE autocomplete:
# System prompt presets
options = ClaudeAgent::AgentOptions.new(
system_prompt: ClaudeAgent::SystemPromptPreset.claude_code
)
# With additional instructions appended
options = ClaudeAgent::AgentOptions.new(
system_prompt: ClaudeAgent::SystemPromptPreset.claude_code("Always use Crystal best practices.")
)
# Tools presets
options = ClaudeAgent::AgentOptions.new(
tools: ClaudeAgent::ToolsPreset.claude_code
)
# String values still work for flexibility
options = ClaudeAgent::AgentOptions.new(
system_prompt: "You are a helpful assistant.",
tools: ["Read", "Write", "Bash"]
)
Custom Tools (SDK MCP Servers)
Define custom tools that run in-process using the SDK MCP server architecture.
# 1. Define tools using Schema builder
greet_tool = ClaudeAgent.tool(
name: "greet",
description: "Greet a user by name",
schema: ClaudeAgent::Schema.object({
"name" => ClaudeAgent::Schema.string("The name to greet"),
}, required: ["name"])
) do |args|
name = args["name"].as_s
ClaudeAgent::ToolResult.text("Hello, #{name}!")
end
calculator_tool = ClaudeAgent.tool(
name: "add",
description: "Add two numbers",
schema: ClaudeAgent::Schema.object({
"a" => ClaudeAgent::Schema.number("First number"),
"b" => ClaudeAgent::Schema.number("Second number"),
}, required: ["a", "b"])
) do |args|
a = args["a"].as_f
b = args["b"].as_f
ClaudeAgent::ToolResult.text("#{a} + #{b} = #{a + b}")
end
# 2. Bundle tools into an SDK MCP server
server = ClaudeAgent.create_sdk_mcp_server(
name: "my-tools",
tools: [greet_tool, calculator_tool]
)
# 3. Configure the agent with the server
mcp_servers = {} of String => ClaudeAgent::MCPServerConfig
mcp_servers["my-tools"] = server
options = ClaudeAgent::AgentOptions.new(
mcp_servers: mcp_servers,
# Tool names follow pattern: mcp__<server>__<tool>
allowed_tools: ["mcp__my-tools__greet", "mcp__my-tools__add"]
)
# 4. Use the agent - tools run in your Crystal process
ClaudeAgent.query("Greet Alice and calculate 2 + 3", options) do |msg|
puts msg if msg.is_a?(ClaudeAgent::AssistantMessage)
end
V2 Streaming Session
For bidirectional communication with send/receive patterns.
require "claude-agent-cr"
# Block form (recommended) - automatically handles cleanup
ClaudeAgent::StreamingSession.open do |session|
session.send("What is 2 + 2?")
session.each_message do |msg|
case msg
when ClaudeAgent::AssistantMessage
puts msg.text if msg.has_text?
when ClaudeAgent::ResultMessage
puts "Done! Cost: $#{msg.cost_usd}"
end
end
end
# Manual control for complex interactions
session = ClaudeAgent::StreamingSession.new
session.start
session.send("Hello!")
msg = session.receive # Blocking receive
session.close
Type-Safe Schema Builder
Crystal's answer to Zod - use the type system to generate JSON schemas for tool definitions and structured outputs.
# Define a schema for tool input or structured output
user_schema = ClaudeAgent::Schema.object({
"name" => ClaudeAgent::Schema.string("User's name"),
"age" => ClaudeAgent::Schema.integer("Age", minimum: 0, maximum: 150),
"email" => ClaudeAgent::Schema.string("Email", format: "email"),
"tags" => ClaudeAgent::Schema.array(ClaudeAgent::Schema.string, "User tags"),
"role" => ClaudeAgent::Schema.enum(["admin", "user", "guest"], "User role"),
"active" => ClaudeAgent::Schema.boolean("Is user active"),
}, required: ["name", "email"])
# Available schema types:
# - Schema.string(description, min_length:, max_length:, pattern:, format:)
# - Schema.integer(description, minimum:, maximum:)
# - Schema.number(description, minimum:, maximum:) # for floats
# - Schema.boolean(description)
# - Schema.array(items_schema, description, min_items:, max_items:)
# - Schema.object(properties, required:, description:, additional_properties:)
# - Schema.enum(values, description) # string literals
# - Schema.optional(schema) # union with null
# - Schema.union(schema1, schema2, ...) # oneOf
# Use with custom tool definition
my_tool = ClaudeAgent.tool(
name: "create_user",
description: "Creates a new user",
schema: user_schema
) do |args|
ClaudeAgent::ToolResult.text("Created user: #{args["name"]}")
end
# Or use with structured outputs (see Structured Outputs section)
Hooks
Intercept tool usage to block or modify actions. All hook inputs include common context fields (session_id, transcript_path, cwd, permission_mode, hook_event_name) plus event-specific fields. The SDK supports all hook events:
| Event | Event-Specific Fields |
|---|---|
| PreToolUse | tool_name, tool_input, tool_use_id |
| PostToolUse | tool_name, tool_input, tool_use_id, tool_result/tool_response |
| PostToolUseFailure | tool_name, tool_input, tool_use_id, error, is_interrupt |
| PermissionRequest | tool_name, tool_input, tool_use_id, permission_suggestions |
| Elicitation | mcp_server_name, elicitation_message, elicitation_mode, elicitation_url, elicitation_id, requested_schema |
| ElicitationResult | mcp_server_name, elicitation_action, elicitation_content, elicitation_id |
| PreCompact | trigger, custom_instructions |
| Notification | notification_message, notification_title, notification_type |
| UserPromptSubmit | user_prompt |
| Stop | stop_hook_active |
| SubagentStart | agent_id, agent_type |
| SubagentStop | agent_id, agent_type, agent_transcript_path, stop_hook_active |
| SessionStart | source ("startup", "resume", "clear", "compact") |
| SessionEnd | session_end_reason ("clear", "logout", etc.) |
| TeammateIdle | agent_id, agent_type — background agent is idle and available |
| TaskCompleted | task_id, task_status, task_summary, tool_use_id |
| ConfigChange | config_change_source, config_change_diff — settings/permissions changed mid-session |
For local hook callbacks, permission_mode is normalized to the CLI-style values such as default, acceptEdits, plan, bypassPermissions, auto, and dontAsk.
HookMatcher also supports an optional timeout value in seconds for control-protocol hook registrations.
# Block 'rm' commands (PreToolUse with tool_use_id)
block_rm = ->(input : ClaudeAgent::HookInput, id : String, ctx : ClaudeAgent::HookContext) {
if input.tool_name == "Bash" && input.tool_input.try(&.["command"]?.try(&.as_s.includes?("rm")))
ClaudeAgent::HookResult.deny("Deletion blocked by policy.")
else
ClaudeAgent::HookResult.allow
end
}
# Audit tool executions (PostToolUse with tool_response and tool_input)
audit_hook = ->(input : ClaudeAgent::HookInput, _id : String, _ctx : ClaudeAgent::HookContext) {
puts "Tool: #{input.tool_name} (#{input.tool_use_id})"
puts "Result: #{input.tool_result}"
ClaudeAgent::HookResult.allow
}
# Archive transcript before compaction (PreCompact with transcript_path)
pre_compact_handler = ->(input : ClaudeAgent::HookInput, _id : String, _ctx : ClaudeAgent::HookContext) {
if path = input.transcript_path
puts "Archiving transcript: #{path}"
# File.copy(path, "/backups/#{input.session_id}.jsonl")
end
ClaudeAgent::HookResult.allow
}
hooks = ClaudeAgent::HookConfig.new(
pre_tool_use: [ClaudeAgent::HookMatcher.new(matcher: "Bash", hooks: [block_rm], timeout: 15.0)],
post_tool_use: [ClaudeAgent::HookMatcher.new(hooks: [audit_hook])],
pre_compact: [pre_compact_handler],
)
options = ClaudeAgent::AgentOptions.new(hooks: hooks)
Subagents
Define specialized agents that can be spawned by the main agent to handle focused subtasks.
agents = {
"code-reviewer" => ClaudeAgent::AgentDefinition.new(
description: "Expert code reviewer",
prompt: "You are an expert code reviewer. Analyze code for quality and issues.",
tools: ["Read", "Glob", "Grep"],
model: "sonnet"
),
"test-writer" => ClaudeAgent::AgentDefinition.new(
description: "Test case generator",
prompt: "Generate comprehensive test cases.",
tools: ["Read", "Write"],
model: "haiku"
),
}
options = ClaudeAgent::AgentOptions.new(
agents: agents,
allowed_tools: ["Read", "Task"], # Task tool required for spawning subagents
)
External MCP Servers
Connect to external MCP (Model Context Protocol) servers to extend Claude's capabilities.
# Build the MCP servers hash
mcp_servers = {} of String => ClaudeAgent::MCPServerConfig
# Stdio server (local process)
mcp_servers["playwright"] = ClaudeAgent::ExternalMCPServerConfig.stdio(
"npx",
["-y", "@playwright/mcp@latest"]
)
# HTTP server (remote)
mcp_servers["docs"] = ClaudeAgent::ExternalMCPServerConfig.http(
"https://code.claude.com/docs/mcp"
)
# SSE server (remote streaming)
mcp_servers["events"] = ClaudeAgent::ExternalMCPServerConfig.sse(
"https://api.example.com/mcp/sse",
headers: {"Authorization" => "Bearer token"}
)
options = ClaudeAgent::AgentOptions.new(
mcp_servers: mcp_servers,
# Allow all tools from these servers (wildcard pattern)
allowed_tools: ["mcp__playwright__*", "mcp__docs__*", "mcp__events__*"]
)
Structured Outputs
Get validated JSON responses matching your schema.
# Define the output schema
schema = ClaudeAgent::Schema.object({
"name" => ClaudeAgent::Schema.string("User name"),
"email" => ClaudeAgent::Schema.string("Email address"),
"age" => ClaudeAgent::Schema.integer("Age"),
}, required: ["name", "email"])
options = ClaudeAgent::AgentOptions.new(
# Use the factory method - it handles conversion automatically
output_format: ClaudeAgent::OutputFormat.json_schema(
schema,
name: "UserInfo",
description: "Extracted user information"
)
)
ClaudeAgent.query("Extract user info from: John Doe, john@example.com, 30", options) do |msg|
if msg.is_a?(ClaudeAgent::ResultMessage)
if structured = msg.structured_output
puts structured.to_pretty_json
# => {"name": "John Doe", "email": "john@example.com", "age": 30}
end
end
end
Session Management
Resume, fork, and continue conversations.
# Continue most recent conversation
options = ClaudeAgent::AgentOptions.new(
continue_conversation: true
)
# Resume a specific session
options = ClaudeAgent::AgentOptions.new(
resume: "session-uuid-here"
)
# Resume at a specific message (precise resume point)
options = ClaudeAgent::AgentOptions.new(
resume: "session-uuid-here",
resume_session_at: "message-uuid-here"
)
# Fork a session (create a branch)
options = ClaudeAgent::AgentOptions.new(
resume: "session-uuid-here",
fork_session: true
)
Skills, Task Budgets, and System Prompts
Recent Claude Code releases add a handful of options that the SDK wires up automatically.
# Enable all installed skills (Skill(*) allow-rule). Also defaults
# setting_sources to ["user", "project"] so the CLI discovers them.
ClaudeAgent::AgentOptions.new(skills: "all")
# Or restrict to a named set
ClaudeAgent::AgentOptions.new(skills: ["git", "playwright"])
# Cache-friendly preset: strips per-user dynamic sections from the prompt.
ClaudeAgent::AgentOptions.new(
system_prompt: ClaudeAgent::SystemPromptPreset.claude_code(
"Focus on code review.",
true, # exclude_dynamic_sections
),
)
# Load the system prompt from a file.
ClaudeAgent::AgentOptions.new(
system_prompt: ClaudeAgent::SystemPromptFile.new("./prompts/reviewer.md"),
)
# API-side token budget awareness and a fixed session title.
ClaudeAgent::AgentOptions.new(
task_budget: ClaudeAgent::TaskBudget.new(120_000),
title: "refactor session",
)
Pre-warming and Context Usage
# Start the subprocess while you prepare the first prompt.
client = ClaudeAgent.startup(ClaudeAgent::AgentOptions.new)
client.query("Return OK")
client.each_response { |msg| puts msg if msg.is_a?(ClaudeAgent::ResultMessage) }
# Inspect context window usage (requires a CLI that supports it).
usage = client.get_context_usage
puts "#{usage.total_tokens}/#{usage.max_tokens} (#{usage.percentage.round(1)}%)"
usage.categories.each { |cat| puts " #{cat.name}: #{cat.tokens}" }
client.stop
Runtime MCP and Plugin Management
ClaudeAgent::AgentClient.open do |client|
client.reload_plugins # refresh plugin directory
client.prompt_suggestion # ask the CLI for a next-step suggestion
# Lazily activate an MCP server that advertises its tools channel on demand.
client.enable_mcp_channel("playwright")
# Replace the active MCP server set without restarting the subprocess.
client.set_mcp_servers({
"fetch" => JSON::Any.new({
"type" => JSON::Any.new("http"),
"url" => JSON::Any.new("https://example.com/mcp"),
}),
})
end
Transcript Controls
ClaudeAgent::AgentClient.open do |client|
# Seed context without triggering an assistant turn.
client.send_user_message("Remember: my favourite colour is teal.", should_query: false)
client.query("What is my favourite colour?")
end
# Include system entries (tool results, status lines) when replaying history.
ClaudeAgent.get_session_messages(
"session-uuid",
directory: Dir.current,
include_system_messages: true,
)
Session Forks and Subagents
# Fork a session into a new branch with fresh UUIDs. Optionally slice the
# transcript up to a specific message UUID and give the fork its own title.
fork = ClaudeAgent.fork_session(
"session-uuid",
directory: Dir.current,
up_to_message_id: "message-uuid",
title: "experimental branch",
)
puts fork.session_id
# Discover subagents that were spawned inside a session and read a transcript.
ClaudeAgent.list_subagents("session-uuid", directory: Dir.current).each do |agent_id|
messages = ClaudeAgent.get_subagent_messages(
"session-uuid",
agent_id,
directory: Dir.current,
)
puts "#{agent_id}: #{messages.size} messages"
end
# Delete a session plus its sibling subagent transcript directory.
ClaudeAgent.delete_session("session-uuid", directory: Dir.current)
Session History
List saved sessions and read the top-level user/assistant messages from a transcript. You can also look up a single session and mutate session metadata.
# List recent sessions for the current project
sessions = ClaudeAgent.list_sessions(directory: Dir.current, limit: 10)
sessions.each do |session|
puts "#{session.summary} (#{session.session_id})"
end
# Read messages from a specific session transcript
messages = ClaudeAgent.get_session_messages(
"session-uuid-here",
directory: Dir.current,
limit: 20,
offset: 0
)
messages.each do |message|
puts "#{message.type}: #{message.message[\"content\"]?}"
end
# Look up one session directly
info = ClaudeAgent.get_session_info("session-uuid-here", directory: Dir.current)
puts info.try(&.summary)
# Rename or tag a session
ClaudeAgent.rename_session("session-uuid-here", "Refactor investigation", directory: Dir.current)
ClaudeAgent.tag_session("session-uuid-here", "experiment", directory: Dir.current)
# Clear a tag
ClaudeAgent.tag_session("session-uuid-here", nil, directory: Dir.current)
File Checkpointing
Track and rewind file changes.
options = ClaudeAgent::AgentOptions.new(
enable_file_checkpointing: true,
replay_user_messages: true,
permission_mode: ClaudeAgent::PermissionMode::AcceptEdits
)
checkpoint_uuid = nil
ClaudeAgent::AgentClient.open(options) do |client|
client.query("Create a file named test.txt")
client.each_response do |msg|
if msg.is_a?(ClaudeAgent::UserMessage) && msg.uuid
checkpoint_uuid = msg.uuid # Save checkpoint
end
end
# Later: rewind to checkpoint
client.rewind_files(checkpoint_uuid.not_nil!) if checkpoint_uuid
end
Sandbox Configuration
Configure sandboxed execution environments for safer operation.
sandbox = ClaudeAgent::SandboxSettings.new(
enabled: true,
auto_allow_bash_if_sandboxed: true,
excluded_commands: ["rm", "sudo"],
network: ClaudeAgent::SandboxNetworkSettings.new(
allow_local_binding: true,
http_proxy_port: 8080
)
)
options = ClaudeAgent::AgentOptions.new(
sandbox: sandbox
)
Extended Thinking
Control extended thinking behavior with the typed thinking config plus effort, or the legacy max_thinking_tokens option.
ThinkingConfig.adaptive→--thinking adaptiveThinkingConfig.disabled→--thinking disabledThinkingConfig.enabled(n)→--max-thinking-tokens n
# Opus 4.7 defaults to xhigh effort inside Claude Code; other tiers are still
# available for backing off to lower latency or dialing up Max effort.
options = ClaudeAgent::AgentOptions.new(
model: "claude-opus-4-7",
thinking: ClaudeAgent::ThinkingConfig.adaptive,
effort: ClaudeAgent::Effort::Xhigh,
)
# Explicit budget with the classic flag
options = ClaudeAgent::AgentOptions.new(
thinking: ClaudeAgent::ThinkingConfig.enabled(10_000),
)
# Legacy fallback still works
legacy = ClaudeAgent::AgentOptions.new(
max_thinking_tokens: 10_000,
)
Interrupt and Dynamic Control
Interrupt an ongoing run, swap models mid-conversation, inspect live state, and request a fresh context usage breakdown — all against an already-started client.
require "claude-agent-cr"
ClaudeAgent::AgentClient.open do |client|
# Kick off a long-running task
client.query("Count from 1 to 100 slowly, one line at a time.")
spawn do
sleep 2.seconds
client.interrupt # Signal the CLI to stop current operation
end
client.each_response do |message|
puts message.text if message.is_a?(ClaudeAgent::AssistantMessage) && message.has_text?
break if message.is_a?(ClaudeAgent::ResultMessage)
end
# Follow up immediately after the interrupt
client.query("Just say hello.")
client.each_response do |message|
puts message.text if message.is_a?(ClaudeAgent::AssistantMessage) && message.has_text?
break if message.is_a?(ClaudeAgent::ResultMessage)
end
# Swap settings live
client.set_permission_mode(ClaudeAgent::PermissionMode::AcceptEdits)
client.set_model("claude-opus-4-7")
# Inspect initialization metadata and live session state
if info = client.get_server_info
puts "Commands available: #{info.commands.size}"
puts "Output style: #{info.output_style || "default"}"
end
mcp_status = client.get_mcp_status
mcp_status.mcp_servers.each do |server|
puts "MCP #{server.name}: #{server.status}"
end
# Context window usage (requires a CLI that supports `get_context_usage`)
begin
usage = client.get_context_usage
puts "#{usage.total_tokens}/#{usage.max_tokens} tokens (#{usage.percentage.round(1)}%)"
usage.categories.each { |category| puts " #{category.name}: #{category.tokens}" }
rescue ex
puts "Context usage not available on this CLI: #{ex.message}"
end
# Refresh plugins / MCP servers without restarting the subprocess
client.reload_plugins
client.enable_mcp_channel("playwright") # only needed for lazy servers
# Cancel a queued follow-up message before the model processes it
queued = UUID.random.to_s
client.send_user_message("Queued follow-up", uuid: queued)
puts "Cancelled queued message: #{client.cancel_async_message(queued)}"
end
Error Handling
Every SDK error inherits from ClaudeAgent::Error. Rescue the specific subclasses for actionable error handling, or the base class to catch everything.
require "claude-agent-cr"
begin
ClaudeAgent::AgentClient.open do |client|
client.query("Hello")
client.each_response do |message|
case message
when ClaudeAgent::AssistantMessage
puts message.text if message.has_text?
when ClaudeAgent::ResultMessage
break
end
end
end
rescue ex : ClaudeAgent::CLINotFoundError
# Claude Code CLI is not installed or not on PATH.
STDERR.puts "Install Claude Code: curl -fsSL https://claude.ai/install.sh | bash"
STDERR.puts "Attempted CLI path: #{ex.cli_path}" if ex.cli_path
rescue ex : ClaudeAgent::ProcessError
# Subprocess exited with a non-zero status; `exit_code` and `stderr` are set.
STDERR.puts "CLI exited #{ex.exit_code}: #{ex.stderr}"
rescue ex : ClaudeAgent::ConnectionError
# Subprocess failed to start, the control-protocol handshake failed, or a
# pending request was cancelled mid-flight.
STDERR.puts "Connection error: #{ex.message}"
rescue ex : ClaudeAgent::JSONDecodeError
# A stream-json line could not be parsed. `raw_data` preserves the payload
# (truncated to 500 chars for diagnostics).
STDERR.puts "Malformed CLI output: #{ex.raw_data}"
rescue ex : ClaudeAgent::TimeoutError
STDERR.puts "Timed out waiting for Claude Code: #{ex.message}"
rescue ex : ClaudeAgent::ConfigurationError
STDERR.puts "Invalid SDK configuration: #{ex.message}"
rescue ex : ClaudeAgent::UnsupportedOptionError
# An AgentOptions flag was rejected by the installed Claude Code CLI.
# `option` is the specific flag (e.g., "--title"); `cli_path` is the
# resolved binary. Upgrade the CLI or stop setting that option.
STDERR.puts "CLI rejected #{ex.option}: #{ex.message}"
rescue ex : ClaudeAgent::Error
# Catch-all for any other SDK-level error.
STDERR.puts "SDK error: #{ex.message}"
end
CLI Capability Probe
Forward-compatible SDK options like task_budget, thinking, and SystemPromptFile map to CLI flags that only newer Claude Code releases understand. When the SDK hands an unknown flag to an older CLI, the subprocess aborts at argv-parse time with error: unknown option '--task-budget', which previously surfaced to callers as an opaque connection-closed error.
To prevent that, CLIClient probes claude --help once per cli_path at start time, caches the advertised flag set, and silently drops any forward-compatible optional flag the CLI does not recognize. A short warning is emitted through your stderr callback (or to standard error) whenever a flag is dropped.
- Core flags (
--model,--print,--output-format,--verbose, etc.) are never filtered. Only the SDK-only forward-compatible flags listed inCLIClient::OPTIONAL_CLI_FLAGSare eligible for filtering. options.titleis not a CLI argument at all. The SDK applies it via the session-file mutation path (ClaudeAgent.rename_session/ thegenerate_session_titlecontrol request). Similarly,SystemPromptPreset#exclude_dynamic_sectionsflows through theinitializecontrol request asexcludeDynamicSections.- Set
AgentOptions#probe_cli_capabilities = falseto disable the probe entirely and pass every option through unmodified. Use this when you know the installed CLI supports every flag you configure and you want to avoid the one-timeclaude --helpsubprocess. - If the CLI ever aborts with an
unknown optionstderr line that slips past the probe, the SDK raises a typedClaudeAgent::UnsupportedOptionErrorwithoptionandcli_pathpopulated, so callers can recover cleanly instead of parsing error strings.
options = ClaudeAgent::AgentOptions.new(
task_budget: ClaudeAgent::TaskBudget.new(120_000), # dropped on older CLIs
thinking: ClaudeAgent::ThinkingConfig.adaptive, # dropped on older CLIs
title: "refactor session", # session-file only
probe_cli_capabilities: true, # default
stderr: ->(line : String) { puts "[claude] #{line}" },
)
begin
ClaudeAgent::AgentClient.open(options) { |client| client.query("Hi") }
rescue ex : ClaudeAgent::UnsupportedOptionError
puts "CLI does not support #{ex.option}; upgrade with `claude upgrade`."
end
ClaudeAgent::AgentOptions Reference
A single place to see every configuration knob the SDK exposes. Every field is optional; uncomment or remove the ones you don't need.
require "claude-agent-cr"
options = ClaudeAgent::AgentOptions.new(
# --- Core model + system prompt ----------------------------------------
model: "claude-opus-4-7", # alias or full model ID
fallback_model: "claude-haiku-4-5", # used on rate-limit or model failure
system_prompt: "You are a helpful assistant.", # String, preset, or file:
# system_prompt: ClaudeAgent::SystemPromptPreset.claude_code(
# "Always use Crystal best practices.",
# true, # exclude_dynamic_sections -> cacheable prefix across users
# ),
# system_prompt: ClaudeAgent::SystemPromptFile.new("./prompt.md"),
append_system_prompt: "Prefer standard library APIs.",
# --- Tools ---------------------------------------------------------------
tools: ClaudeAgent::ToolsPreset.claude_code, # or Array(String)
allowed_tools: ["Read", "Glob", "Grep", "Task"],
disallowed_tools: ["Bash"],
skills: "all", # or ["git", "playwright"]
# --- Permissions ---------------------------------------------------------
permission_mode: ClaudeAgent::PermissionMode::AcceptEdits,
# Other modes: Default, Plan, BypassPermissions, Auto, DontAsk
allow_dangerously_skip_permissions: false,
permission_prompt_tool_name: nil, # delegate prompts to a tool
can_use_tool: nil, # PermissionCallback
# --- Budgets & turn limits ----------------------------------------------
max_turns: 10,
max_budget_usd: 1.0,
task_budget: ClaudeAgent::TaskBudget.new(120_000), # API-side token budget
title: "refactor session", # fixed session title
probe_cli_capabilities: true, # default; filters forward-only flags
# --- Extended thinking ---------------------------------------------------
thinking: ClaudeAgent::ThinkingConfig.adaptive, # adaptive | enabled(n) | disabled
effort: ClaudeAgent::Effort::Xhigh, # Low | Medium | High | Xhigh | Max
max_thinking_tokens: nil, # legacy fallback
# --- MCP servers ---------------------------------------------------------
mcp_servers: {
"playwright" => ClaudeAgent::ExternalMCPServerConfig.stdio(
"npx", ["-y", "@playwright/mcp@latest"],
).as(ClaudeAgent::MCPServerConfig),
},
strict_mcp_config: false,
# --- Subagents -----------------------------------------------------------
agents: {
"reviewer" => ClaudeAgent::AgentDefinition.new(
description: "Read-only code reviewer",
prompt: "Audit code without modifying it.",
tools: ["Read", "Glob", "Grep"],
disallowed_tools: ["Bash", "Write", "Edit"],
model: "sonnet",
max_turns: 3,
),
},
agent: "reviewer", # boot a specific subagent
# --- Hooks & async features ---------------------------------------------
hooks: ClaudeAgent::HookConfig.new, # (register matchers here)
on_elicitation: nil, # MCP user-input callback
prompt_suggestions: true,
agent_progress_summaries: true,
include_partial_messages: true, # + fine-grained streaming
replay_user_messages: false,
# --- Output & structured responses --------------------------------------
output_format: nil, # OutputFormat.json_schema(...)
# --- CLI / environment ---------------------------------------------------
cli_path: nil, # override discovery
env: {"CLAUDE_AGENT_TRACING" => "1"},
betas: ["extended-thinking-2025-01-24"],
add_dirs: ["./vendor"],
plugins: ["./plugins/my-plugin"],
cwd: Dir.current,
user: "alice",
stderr: ->(line : String) { STDERR.puts(line) }, # observe CLI stderr
max_buffer_size: nil, # override stream buffer
# --- Settings sources ----------------------------------------------------
setting_sources: ["user", "project"], # [] disables all
settings_path: nil, # explicit settings.json
# --- Session management --------------------------------------------------
continue_conversation: false,
resume: nil, # session ID to resume
resume_session_at: nil, # message UUID to resume from
session_id: nil, # explicit session ID
fork_session: false, # fork on resume
no_session_persistence: false,
# --- Safety & checkpointing ---------------------------------------------
enable_file_checkpointing: true,
sandbox: ClaudeAgent::SandboxSettings.new(enabled: true),
)
ClaudeAgent.query("Review the current directory.", options) do |message|
puts message
end
Status
Developed against Claude Code CLI v2.1.84+. The SDK is forward-compatible: features that require newer CLI versions are gracefully no-op or surface a clear error on older CLIs.
| Feature | Status | Notes |
|---|---|---|
| One-shot queries | ✅ Working | Single-turn queries work reliably |
| Interactive sessions | ✅ Working | Multi-turn conversations with tool use |
| Custom tools (in-process) | ✅ Working | SDK MCP servers via control protocol |
| External MCP Servers | ✅ Working | Stdio, HTTP, and SSE transports |
| Runtime MCP Management | ✅ Working | set_mcp_servers, enable_mcp_channel, reload_plugins |
| Schema Builder | ✅ Working | Type-safe JSON schema generation |
| Hooks | ✅ Working | PreToolUse, PostToolUse, PreCompact, Notification, TeammateIdle, TaskCompleted, ConfigChange, and more |
| V2 Streaming | ✅ Working | Send/receive patterns |
| Subagents | ✅ Working | Rich AgentDefinition fields for specialized agents |
| Structured Outputs | ✅ Working | JSON schema validation |
| Session Management | ✅ Working | Resume, fork, continue, resume_session_at, delete |
| Session Fork / Subagent Transcripts | ✅ Working | fork_session, list_subagents, get_subagent_messages, delete_session |
| Dynamic Controls | ✅ Working | Model, permissions, MCP status, flag settings, remote control, proactive mode, session titles, get_context_usage, prompt_suggestion |
| Permission Modes | ✅ Working | default, acceptEdits, plan, bypassPermissions, auto, dontAsk |
| Skills | ✅ Working | "all" or a named list; auto-injects Skill/Skill(name) and default setting sources |
| Task Budgets | ✅ Working | task_budget + title options |
| System Prompt Variants | ✅ Working | Strings, SystemPromptPreset (with exclude_dynamic_sections), and SystemPromptFile |
| Server Info | ✅ Working | Access initialization metadata like commands and output styles |
| Rich Event Messages | ✅ Working | Typed init/task/rate-limit/prompt-suggestion/elicitation events, ApiRetryMessage, MemoryRecallMessage, StatusMessage, MirrorErrorMessage, and unknown-message fallback |
| Hook Propagation | ✅ Working | Hook callbacks can modify inputs and return CLI hook outputs |
| Permission Propagation | ✅ Working | Permission callbacks can update input, permissions, interrupt; context now exposes tool_use_id, agent_id, blocked_path |
| Session History | ✅ Working | List, inspect, rename, and tag local sessions |
| File Checkpointing | ✅ Working | Track and rewind file changes |
| Sandbox Configuration | ✅ Working | Full sandbox settings support |
| Extended Thinking | ✅ Working | thinking (adaptive/enabled/disabled), effort (including xhigh), max_thinking_tokens |
| Pre-warming | ✅ Working | ClaudeAgent.startup starts the CLI without sending a prompt |
| Trace Propagation | ✅ Working | Forwards TRACEPARENT/TRACESTATE env vars to the CLI |
| Unknown Content Types | ✅ Working | Graceful handling of future content block types |
| Compact Boundary | ✅ Working | Detect when CLI compacts session history |
Handling Compact Boundaries
When the CLI compacts a session, it emits a CompactBoundaryMessage:
client.each_response do |message|
case message
when ClaudeAgent::CompactBoundaryMessage
puts "Session compacted: #{message.compact_metadata.trigger}"
puts "Pre-compaction tokens: #{message.compact_metadata.pre_tokens}"
end
end
Tool Use ID Access
Tool use IDs are available on ToolUseBlock within AssistantMessage.content:
when ClaudeAgent::AssistantMessage
message.content.each do |block|
if block.is_a?(ClaudeAgent::ToolUseBlock)
puts "Tool: #{block.name}, ID: #{block.id}"
end
end
end
Thinking Content Access
For extended thinking models, ResultMessage.stop_reason is also available to inspect why the turn ended.
when ClaudeAgent::AssistantMessage
message.content.each do |block|
case block
when ClaudeAgent::ThinkingBlock
puts "Thinking: #{block.thinking}"
when ClaudeAgent::RedactedThinkingBlock
puts "Redacted thinking (signature present)"
end
end
when ClaudeAgent::ResultMessage
puts "Stop reason: #{message.stop_reason}"
end
When include_partial_messages is enabled, the SDK also opts into fine-grained tool streaming so StreamEvent payloads can include incremental delta events such as content_block_delta and input_json_delta when the CLI emits them.
Changelog
0.6.0
Hardening release. Surfaced latent bugs across wire protocol, robustness, and error handling via a multi-angle audit against the Python SDK + live Claude Code CLI v2.1.114, and fixed the high-severity findings.
Breaking changes (upgrade notes):
- Removed
ClaudeAgent::PermissionDeniedError. The class was declared inerrors.crbut never raised anywhere in the SDK. Anyrescue ex : ClaudeAgent::PermissionDeniedErrorclauses in consumer code will now fail to compile — the branch was never executing, so it's safe to delete. MCPToolAnnotationsfields renamed to MCP-spec*Hintkeys:#read_only→#read_only_hint?(JSONreadOnly→readOnlyHint)#destructive→#destructive_hint?(JSONdestructive→destructiveHint)#open_world→#open_world_hint?(JSONopenWorld→openWorldHint)- Plus new
#idempotent_hint?and#title. The old keys were never what the CLI actually emitted; any code reading the old getters was always seeingnil.
QueryIterator#nextnow re-raises errors from the background fiber (CLI not found, subprocess crashed, unsupported option) instead of silently yieldingIterator::Stop. Callers iteratingClaudeAgent.query(prompt)(the non-block form) must now rescue or propagate.AgentOptions#skillsrejects unrecognized strings. Only"all", anArray(String)of skill names, ornilare accepted. Passing"none","off", a single skill name, etc. now raisesConfigurationErrorinstead of being silently ignored.AgentOptions#resume_session_atwithoutresumenow raisesConfigurationErrorat argv build time. Previously this caused an opaque subprocess crash during handshake.AgentOptions#skills: []is a true no-op. Empty array no longer defaultssetting_sourcesto["user","project"]as a side effect. If you were relying on that side effect, setsetting_sourcesexplicitly.AgentOptions#skills: "all"is no longer emitted on the initialize request. Matches Python SDK wire format ("all"and omitted are equivalent at the wire level). Injection of theSkillallow-rule still happens client-side.InitMessage#memory_pathsandMemoryRecallMessage#memory_pathsareHash(String, String)(namespace → path), notArray(String). Reflects what the CLI actually emits.opts.agentsis no longer forwarded as--agentson argv. Agent definitions flow through theinitializecontrol request exclusively (matches Python and TypeScript SDKs).--setting-sourcesalways emits a single=-joined token (e.g.,--setting-sources=user,project). Previously the populated case used the two-token form.ClaudeAgent::CLIClient#send_sdk_initremoved. It was dead code with a broken wire shape (missingrequest_id).
Robustness:
- Fix:
CLIClient#stopnow runs a timed terminate → kill cascade (5s graceful, 5s SIGTERM, 2s SIGKILL) instead of blocking forever onProcess#wait. Fixes the "hang when the CLI is alive but unresponsive" class of bugs. - Fix: Unknown
control_requestsubtypes now parse to a typedControlUnknownRequestfallback and reply with acontrol_responseerror, instead of silently raising and leaving the CLI waiting forever. NewControlUnknownRequeststruct added to theControlRequestInnerunion. - Fix:
claude --helpprobe now has a 5-second timeout and is killed if it hangs (e.g., CLI stuck on an auth prompt). - Fix: User-supplied hook callbacks (for
SessionStart,SessionEnd,UserPromptSubmit,PostToolUse,SubagentStart,SubagentStop,Stop) are now wrapped in a rescue and log to thestderrcallback. Previously, a callback that raised crashed the response reader fiber, skipping cleanup of pending control requests and leaking the subprocess. - Fix:
AgentClient#stopnow closes the message channel before stopping the CLI subprocess, so fibers blocked insideeach_responsewake immediately on shutdown. - Fix: The rolling
stderr_tailbuffer is now cleared on eachstart, sodetect_unknown_option_errornever surfaces a false positive from a previous session. - Fix: Malformed stream-json lines now route through the configured
stderrcallback (with the offending line included, truncated) instead of rawSTDERR.puts. The SDK'sJSONDecodeErrorclass is now actually used to carry the payload.
Wire protocol (non-breaking fixes):
- Fix: The capability probe no longer pre-emptively strips flags. The
--helpregex produced false negatives on current CLI versions (--task-budget,--thinking,--system-prompt-fileare hidden from help but still accepted). Silently dropping a safety cap liketask_budgetwas worse than letting the CLI fail with an actionableUnsupportedOptionError. The probe remains for diagnostics and error translation. - Fix:
ControlResponse.mcp_responsenow serializes the optional JSON-RPCdatafield on error responses.
Typed messages / new fields (additive):
- New:
AssistantMessageBodyexposesid,usage,stop_reason,stop_sequence,container, andcontext_management.AssistantMessagegains forwarders:#message_id,#usage,#stop_reason,#stop_sequence. Previously these fields were silently dropped — breaking token accounting for any SDK user. - New:
InitMessage/ServerInfotypetools,skills,mcp_servers,plugins,cwd,model,permission_mode,fast_mode_state, andclaude_code_version. Raw data remains available viaserver_info.raw_data. - New:
ControlUnknownRequestfallback struct in theControlRequestInnerunion.
Test suite:
- 52 new specs across
spec/stream_parsing_spec.cr(subprocess buffering edges),spec/integration_spec.cr(mocked pipeline, always run in CI),spec/errors_spec.cr(error class construction),spec/meta_spec.cr(version + CHANGELOG sync health). Total test count: 391.
Documentation:
- Hook-events table in README extended with
TeammateIdle,TaskCompleted, andConfigChange. - Permission-mode enumeration in docs now includes
autoanddontAsk.
0.5.1
Audit release: validated every SDK option, message field, and control-request shape against a live Claude Code CLI (v2.1.114) and the official Python SDK. Fixed the wire-format mismatches that were causing subprocess crashes or silent data loss.
Argv / CLI flag fixes:
- Fix:
options.titleis no longer forwarded as--title(that flag has never existed). Titles now flow through theinitializecontrol request as thetitlefield and are applied viaClaudeAgent.rename_session/generate_session_titlewhere needed. - Fix:
SystemPromptPreset#exclude_dynamic_sectionsnow flows through theinitializecontrol request asexcludeDynamicSectionsinstead of a non-existent--exclude-dynamic-sectionsCLI flag. - Fix:
permission_prompt_tool_namenow emits--permission-prompt-tool(the real CLI flag) instead of the non-existent--permission-prompt-tool-name. - Fix:
enable_file_checkpointingsets theCLAUDE_CODE_ENABLE_SDK_FILE_CHECKPOINTING=trueenvironment variable instead of emitting a non-existent--enable-file-checkpointingflag. - Fix:
--betasis now emitted as separate variadic tokens (e.g.,--betas beta1 beta2) instead of a joined string, matching the CLI's variadic<betas...>parser. - Fix:
--allowedToolsand--disallowedToolsnow use comma separators (matching the canonical form used by the Python SDK). - Fix:
ToolsPreset.claude_codenow maps to--tools default(the canonical CLI value) instead of the SDK-internal preset nameclaude_code. - Fix: When
system_promptis nil, the SDK emits--system-prompt ""so the subprocess runs in vanilla mode (matching Python/TS) rather than falling back to the interactive Claude Code default prompt. - Fix: SDK MCP servers are now declared in
--mcp-configwithtype: "sdk"so the CLI knows to route tool calls back asmcp_messagecontrol requests. Previously, SDK servers were silently dropped from the argv.
Wire-format fixes:
- Fix: The
shouldQueryfield on outgoing user messages is emitted in camelCase (matching the TS SDK'sSDKUserMessage.shouldQuery) instead of snake_caseshould_query. - Fix:
ControlResponse.mcp_responsenow nests the JSON-RPC payload under the standardresponsewrapper ({subtype, request_id, response: {mcp_response: ...}}) instead of placingmcp_responseat the top level. - Fix:
InitMessage#memory_pathsandMemoryRecallMessage#memory_pathsnow return the correct shape from the CLI (see 0.6.0 for the breaking type change).
New capabilities / fields:
- New:
ResultMessageexposesapi_error_status,model_usage,permission_denials, andfast_mode_state, matching the fields the CLI actually emits. - New:
AgentOptions#include_hook_eventsmaps to--include-hook-eventsand emits hook lifecycle events (hook_started,hook_progress,hook_response) into the output stream. - New:
AgentOptions#extra_argsaccepts aHash(String, String?)of arbitrary CLI flags for forward compatibility with newly-added Claude Code flags the SDK has not yet modeled. - New:
SandboxSettings#fail_if_unavailable(defaulttrue, matching TS v0.2.91+): when sandboxing is enabled but dependencies are missing, the CLI fails fast by default instead of silently running unsandboxed. - New:
CLIClientprobes the installed CLI withclaude --helponce percli_pathand silently drops any forward-compatible SDK-only flag (--task-budget,--thinking,--system-prompt-file) the CLI does not advertise. Core flags are never filtered. - New:
ClaudeAgent::UnsupportedOptionErroris raised when an "unknown option" stderr message is detected during a connection failure, carrying the offending flag name and the CLI path. - New:
AgentOptions#probe_cli_capabilities(defaulttrue) toggles the capability probe.
0.5.0
- New:
skillsoption onAgentOptionsaccepting"all", a list of skill names, or an empty list; automatically injectsSkill/Skill(name)entries intoallowed_toolsand defaultssetting_sourcesto["user","project"] - New:
SystemPromptFilevariant for--system-prompt-file, andexclude_dynamic_sectionsonSystemPromptPresetfor cacheable prompts - New:
task_budgetoption (--task-budget) andtitleoption (--title) - New:
PermissionMode::AutoandPermissionMode::DontAsk - New:
Effort::Xhightier, which Claude Code uses as the default for Claude Opus 4.7 - New: Thinking config now maps to the correct CLI flags:
--thinking adaptive,--thinking disabled, or--max-thinking-tokens <n>forenabled - New: Expanded
AgentDefinitionfields —disallowed_tools,skills,memory,mcp_servers,initial_prompt,max_turns,background,effort,permission_mode - New: Runtime MCP / plugin control methods —
AgentClient#get_context_usage,reload_plugins,prompt_suggestion,set_mcp_servers,enable_mcp_channel - New: Typed
ContextUsageResponsewith per-category token usage, model info, and autocompact thresholds - New: Session helpers —
ClaudeAgent.fork_session,ClaudeAgent.delete_session,ClaudeAgent.list_subagents,ClaudeAgent.get_subagent_messages - New:
include_system_messagesoption onClaudeAgent.get_session_messages - New:
should_query: falseonsend_user_message/queryappends a message without triggering an assistant turn - New:
ClaudeAgent.startuppre-warms the Claude Code subprocess and returns a readyAgentClient - New: Additional hook events —
TeammateIdle,TaskCompleted,ConfigChange - New:
PermissionContextgainstool_use_id,agent_id, andblocked_path - New:
ResultMessagegainsterminal_reasonanderrorsfields - New: Typed system messages —
ApiRetryMessage,MemoryRecallMessage,StatusMessage(for therequestingstatus), and top-levelMirrorErrorMessage - New:
InitMessage#memory_pathsaccessor;MCPServerStatusgainscapabilities - New: Automatic W3C trace context propagation (
TRACEPARENT/TRACESTATE) - New: Empty
setting_sources=[]now emits a single--setting-sources=token instead of being misparsed - New: Eight new examples — 33 (session forks & subagents), 34 (advanced subagent definitions), 35 (auto/dontAsk permission modes), 36 (transcript controls), 37 (forward-compatible options), 38 (context usage + startup), 39 (new hooks), 40 (runtime MCP/plugin controls)
0.4.0
- New: Session history helpers:
ClaudeAgent.list_sessions,ClaudeAgent.get_session_info, andClaudeAgent.get_session_messages - New: Session metadata mutation helpers:
ClaudeAgent.rename_sessionandClaudeAgent.tag_session - New: Dynamic control APIs:
set_permission_mode,set_model,get_mcp_status,reconnect_mcp_server,toggle_mcp_server,stop_task,apply_flag_settings,enable_remote_control,set_proactive,generate_session_title - New:
get_server_infofor initialization metadata like commands and output styles - New: Richer system/event message typing:
InitMessage,TaskStartedMessage,TaskProgressMessage,TaskNotificationMessage,UnknownMessage - New: Typed
RateLimitEventandRateLimitInfo - Improved:
include_partial_messagesnow enables fine-grained tool streaming for richerStreamEventdeltas - New: TypeScript-parity
prompt_suggestions,PromptSuggestionMessage,settings, andcancel_async_message - New: TypeScript-parity MCP elicitation callbacks, hook events, and
ElicitationCompleteMessage - New: Typed thinking config support (
thinking,effort) andResultMessage.stop_reason - New: Optional live E2E specs covering dynamic controls, session history, hook overrides, task events, structured output, and typed init metadata
- Improved: Hook matcher timeout support and initialization payload parity for hooks and agents
- Improved: Hook and permission callbacks now propagate runtime updates through the CLI control protocol
0.3.0
- New: Common context fields on all
HookInputevents:session_id,transcript_path,cwd,permission_mode,hook_event_name - New: Event-specific fields matching official SDKs:
tool_use_id,tool_response,error,is_interrupt,stop_hook_active,agent_id,agent_type,agent_transcript_path,notification_type,source,session_end_reason,permission_suggestions - New:
PermissionRequesthook event for visibility into permission dialogs - New:
CompactBoundaryMessagetype for detecting session compaction (TypeScript SDK feature) - New: Examples for hook lifecycle (20), tool auditing (21), PreCompact archiving (22), and PermissionRequest (23)
- For control protocol hooks, CLI-provided values (e.g.,
transcript_path) take precedence over locally-derived values
Notes on requested features vs official SDKs:
error_type/error_codefields in ResultMessage: Not in official SDKs - usesubtypeandis_errorinsteadSessionInfowithmessage_count/current_tokens: Not in official SDKs - usenum_turnsandusageon ResultMessageAssistantMessage.tool_use_id: Not in official SDKs - access viaToolUseBlock.idin content blocks
0.2.0
- New:
UnknownBlockfor graceful handling of unknown content block types (forward-compatible) - New:
Notificationhook for agent status updates (forward to Slack, dashboards, etc.) - New:
resume_session_atoption for precise message-level session resumption - New:
HookInputfields for PreCompact (trigger,custom_instructions) and Notification (notification_message,notification_title) - Fix: ControlHookCallbackRequest now properly dispatched (PreCompact and other hooks now work)
- Fix: Hook callback routing improved with proper input field extraction
Verified Examples
The following examples have been tested and verified with CLI v2.1.84:
- One-shot queries and streaming (examples 01, 02, 03)
- Tool restrictions and permission modes (examples 04, 05)
- Permission callbacks and SDK MCP servers (examples
06_permission_callback,06_sdk_mcp_server) - Interactive sessions, permission handling, and chat flows (examples 07, 08, 09, 10)
- Local tool definitions and MCP server testing (examples 11, 12)
- Hooks, audit hooks, and lifecycle hooks (examples 13, 20, 21, 22, 28)
- V2 streaming sessions (example 14)
- Subagents and rich task events (examples 15, 27)
- Structured JSON output (example 16)
- Playwright MCP integration (example 17)
- Session history, dynamic controls, and server info (examples 25, 26, 29)
- Session mutation APIs (example 30)
- Prompt suggestions and async message controls (example 31)
- MCP elicitation callback surface (example 32)
- Session fork / subagent transcript helpers (example 33)
- Expanded
AgentDefinitionfields (example 34) - Auto/DontAsk permission modes and rich permission context (example 35)
- Transcript controls —
should_query,include_system_messages(example 36) - Forward-compatible options — skills, task_budget, title, SystemPromptFile (example 37)
get_context_usage+startuppre-warming (example 38)- New hook events — TeammateIdle, TaskCompleted, ConfigChange (example 39)
- Runtime MCP + plugin controls (example 40)
Examples with environment-dependent behavior:
examples/18_mcp_github.crrequiresGITHUB_TOKENexamples/19_mcp_remote.crdepends on remote MCP availabilityexamples/23_hook_permission_request.cronly shows the hook when Claude would actually prompt for permission in the current environmentexamples/32_elicitation_support.crrequiresELICITATION_MCP_URLand a compatible MCP server that actually requests user inputexamples/38_context_usage.cr,examples/39_new_hooks.cr, andexamples/40_mcp_runtime.crexercise features that require a newer Claude Code CLI; older CLIs degrade gracefully
Quick guide:
- Best local/default examples:
01,02,07,14,16,25,26,27,28,29,31,33,36,37 - Best hook-focused examples:
13,20,21,22,23,28,39 - Best MCP-focused examples:
06_sdk_mcp_server,12,17,18,19,40 - Best session-focused examples:
25,30,33,36
Optional Live E2E Specs
The main spec suite is deterministic and does not require a live Claude connection. For parity-critical live checks against the real Claude CLI, run:
CLAUDE_AGENT_RUN_E2E=1 crystal spec spec/e2e_spec.cr
These optional E2E specs cover live initialization metadata, dynamic controls, session history, hook-based tool input overrides, rich task events, structured outputs, and typed init-message metadata.
Contributing
- Fork it (https://github.com/amscotti/claude-agent-cr/fork)
- Create your feature branch (
git checkout -b my-new-feature) - Commit your changes (
git commit -am 'Add some feature') - Push to the branch (
git push origin my-new-feature) - Create a new Pull Request
License
MIT
claude-agent-cr
- 1
- 0
- 0
- 0
- 1
- 7 days ago
- January 24, 2026
MIT License
Sun, 19 Apr 2026 22:20:57 GMT