claude_agent_sdk
claude_agent_sdk copied to clipboard
An Elixir SDK for Claude Code - provides programmatic access to Claude Code CLI with streaming message processing
Claude Agent SDK for Elixir
An Elixir SDK aiming for feature-complete parity with the official claude-agent-sdk-python. Build AI-powered applications with Claude using a production-ready interface for the Claude Code CLI, featuring streaming responses, lifecycle hooks, permission controls, and in-process tool execution via MCP.
Note: This SDK does not bundle the Claude Code CLI. You must install it separately (see Prerequisites).
What You Can Build
- AI coding assistants with real-time streaming output
- Automated code review pipelines with custom permission policies
- Multi-agent workflows with specialized personas
- Tool-augmented applications using the Model Context Protocol (MCP)
- Interactive chat interfaces with typewriter-style output
Installation
Add to your mix.exs:
def deps do
[
{:claude_agent_sdk, "~> 0.9.0"}
]
end
Then fetch dependencies:
mix deps.get
Prerequisites
Install the Claude Code CLI (requires Node.js):
npm install -g @anthropic-ai/claude-code
Verify installation:
claude --version
Quick Start
1. Authenticate
Choose one method:
# Option A: Environment variable (recommended for CI/CD)
export ANTHROPIC_API_KEY="sk-ant-api03-..."
# Option B: OAuth token
export CLAUDE_AGENT_OAUTH_TOKEN="sk-ant-oat01-..."
# Option C: Interactive login
claude login
2. Run Your First Query
alias ClaudeAgentSDK.{ContentExtractor, Options}
# Simple query with streaming collection
ClaudeAgentSDK.query("Write a function that calculates factorial in Elixir")
|> Enum.each(fn msg ->
case msg.type do
:assistant -> IO.puts(ContentExtractor.extract_text(msg) || "")
:result -> IO.puts("Done! Cost: $#{msg.data.total_cost_usd}")
_ -> :ok
end
end)
3. Real-Time Streaming
alias ClaudeAgentSDK.Streaming
{:ok, session} = Streaming.start_session()
Streaming.send_message(session, "Explain GenServers in one paragraph")
|> Stream.each(fn
%{type: :text_delta, text: chunk} -> IO.write(chunk)
%{type: :message_stop} -> IO.puts("")
_ -> :ok
end)
|> Stream.run()
Streaming.close_session(session)
Authentication
The SDK supports three authentication methods, checked in this order:
| Method | Environment Variable | Best For |
|---|---|---|
| OAuth Token | CLAUDE_AGENT_OAUTH_TOKEN |
Production / CI |
| API Key | ANTHROPIC_API_KEY |
Development |
| CLI Login | (uses claude login session) |
Local development |
Cloud Providers
AWS Bedrock:
export CLAUDE_AGENT_USE_BEDROCK=1
export AWS_ACCESS_KEY_ID=...
export AWS_SECRET_ACCESS_KEY=...
export AWS_REGION=us-west-2
Google Vertex AI:
export CLAUDE_AGENT_USE_VERTEX=1
export GOOGLE_APPLICATION_CREDENTIALS=/path/to/key.json
export GOOGLE_CLOUD_PROJECT=your-project-id
Token Setup (Local Development)
For persistent authentication without re-login:
mix claude.setup_token
Check authentication status:
alias ClaudeAgentSDK.AuthChecker
diagnosis = AuthChecker.diagnose()
# => %{authenticated: true, auth_method: "Anthropic API", ...}
Core Concepts
Choosing the Right API
| API | Use Case | When to Use |
|---|---|---|
query/2 |
Simple queries | Batch processing, scripts |
Streaming |
Typewriter UX | Chat interfaces, real-time output |
Client |
Full control | Multi-turn agents, tools, hooks |
Query API
The simplest way to interact with Claude:
# Basic query
messages = ClaudeAgentSDK.query("What is recursion?") |> Enum.to_list()
# With options
opts = %ClaudeAgentSDK.Options{
model: "sonnet",
max_turns: 5,
output_format: :stream_json
}
messages = ClaudeAgentSDK.query("Explain OTP", opts) |> Enum.to_list()
# Streamed input prompts (unidirectional)
prompts = [
%{"type" => "user", "message" => %{"role" => "user", "content" => "Hello"}},
%{"type" => "user", "message" => %{"role" => "user", "content" => "How are you?"}}
]
ClaudeAgentSDK.query(prompts, opts) |> Enum.to_list()
# Custom transport injection
ClaudeAgentSDK.query("Hello", opts, {ClaudeAgentSDK.Transport.Port, []})
|> Enum.to_list()
# Continue a conversation
ClaudeAgentSDK.continue("Can you give an example?") |> Enum.to_list()
# Resume a specific session
ClaudeAgentSDK.resume("session-id", "What about supervision trees?") |> Enum.to_list()
Streaming API
For real-time, character-by-character output:
alias ClaudeAgentSDK.{Options, Streaming}
{:ok, session} = Streaming.start_session(%Options{model: "haiku"})
# Send messages and stream responses
Streaming.send_message(session, "Write a haiku about Elixir")
|> Enum.each(fn
%{type: :text_delta, text: t} -> IO.write(t)
%{type: :tool_use_start, name: n} -> IO.puts("\nUsing tool: #{n}")
%{type: :message_stop} -> IO.puts("\n---")
_ -> :ok
end)
# Multi-turn conversation (context preserved)
Streaming.send_message(session, "Now write one about Phoenix")
|> Enum.to_list()
Streaming.close_session(session)
Subagent Streaming: When Claude spawns subagents via the Task tool, events include a parent_tool_use_id field to identify the source. Main agent events have nil, subagent events have the Task tool call ID. Streaming events also include uuid, session_id, and raw_event metadata for parity with the Python SDK. Stream event wrappers require uuid and session_id (missing keys raise). See the Streaming Guide for details.
Hooks System
Intercept and control agent behavior at key lifecycle points:
alias ClaudeAgentSDK.{Client, Options}
alias ClaudeAgentSDK.Hooks.{Matcher, Output}
# Block dangerous commands
check_bash = fn input, _id, _ctx ->
case input do
%{"tool_name" => "Bash", "tool_input" => %{"command" => cmd}} ->
if String.contains?(cmd, "rm -rf") do
Output.deny("Dangerous command blocked")
else
Output.allow()
end
_ -> %{}
end
end
opts = %Options{
hooks: %{
pre_tool_use: [Matcher.new("Bash", [check_bash])]
}
}
{:ok, client} = Client.start_link(opts)
Available Hook Events:
-
pre_tool_use/post_tool_use- Before/after tool execution -
user_prompt_submit- Before sending user messages -
stop/subagent_stop- Completion events -
pre_compact- Before context compaction
Note: SessionStart, SessionEnd, and Notification hook events are not supported by the Python SDK and are rejected for parity.
See the Hooks Guide for comprehensive documentation.
Permission System
Fine-grained control over tool execution:
alias ClaudeAgentSDK.{Options, Permission.Result}
permission_callback = fn ctx ->
case ctx.tool_name do
"Write" ->
# Redirect system file writes to safe location
if String.starts_with?(ctx.tool_input["file_path"], "/etc/") do
safe_path = "/tmp/sandbox/" <> Path.basename(ctx.tool_input["file_path"])
Result.allow(updated_input: %{ctx.tool_input | "file_path" => safe_path})
else
Result.allow()
end
_ ->
Result.allow()
end
end
opts = %Options{
can_use_tool: permission_callback,
permission_mode: :default # :default | :accept_edits | :plan | :bypass_permissions | :delegate | :dont_ask
}
Note: can_use_tool is mutually exclusive with permission_prompt_tool. The SDK routes can_use_tool through the control client (including string prompts), auto-enables include_partial_messages, and sets permission_prompt_tool to \"stdio\" internally so the CLI can emit permission callbacks. Use :default or :plan for built-in tool permissions; :delegate is intended for external tool execution. Hook-based fallback only applies in non-:delegate modes and ignores updated_permissions. If you do not see callbacks, your CLI build may not emit control callbacks (see examples/advanced_features/permissions_live.exs).
Stream a single client response until the final result:
Client.receive_response_stream(client)
|> Enum.to_list()
MCP Tools (In-Process)
Define custom tools that Claude can call directly in your application:
defmodule MyTools do
use ClaudeAgentSDK.Tool
deftool :calculate, "Perform a calculation", %{
type: "object",
properties: %{
expression: %{type: "string", description: "Math expression to evaluate"}
},
required: ["expression"]
} do
def execute(%{"expression" => expr}) do
# Your logic here
result = eval_expression(expr)
{:ok, %{"content" => [%{"type" => "text", "text" => "Result: #{result}"}]}}
end
end
end
# Create an MCP server with your tools
server = ClaudeAgentSDK.create_sdk_mcp_server(
name: "calculator",
version: "1.0.0",
tools: [MyTools.Calculate]
)
opts = %ClaudeAgentSDK.Options{
mcp_servers: %{"calc" => server},
allowed_tools: ["mcp__calc__calculate"]
}
Note: MCP server routing only supports initialize, tools/list, tools/call, and notifications/initialized. Calls to resources/list or prompts/list return JSON-RPC method-not-found errors to match the Python SDK.
If version is omitted, it defaults to "1.0.0".
Configuration Options
Key options for ClaudeAgentSDK.Options:
| Option | Type | Description |
|---|---|---|
model |
string | "sonnet", "opus", "haiku" |
max_turns |
integer | Maximum conversation turns |
system_prompt |
string | Custom system instructions |
output_format |
atom/map | :text, :json, :stream_json, or JSON schema (SDK enforces stream-json for transport; JSON schema still passed) |
allowed_tools |
list | Tools Claude can use |
permission_mode |
atom | :default, :accept_edits, :plan, :bypass_permissions, :delegate, :dont_ask |
hooks |
map | Lifecycle hook callbacks |
mcp_servers |
map or string | MCP server configurations (or JSON/path alias for mcp_config) |
cwd |
string | Working directory for file operations |
timeout_ms |
integer | Command timeout (default: 75 minutes) |
max_buffer_size |
integer | Maximum JSON buffer size (default: 1MB, overflow yields CLIJSONDecodeError) |
CLI path override: set path_to_claude_code_executable or executable in Options (Python cli_path equivalent).
SDK Logging
The SDK uses its own log level filter (default: :warning) to keep output quiet in dev. Configure via application env:
config :claude_agent_sdk, log_level: :warning # :debug | :info | :warning | :error | :off
Option Presets
alias ClaudeAgentSDK.OptionBuilder
# Environment-based presets
OptionBuilder.build_development_options() # Permissive, verbose
OptionBuilder.build_production_options() # Restrictive, safe
OptionBuilder.for_environment() # Auto-detect from Mix.env()
# Use-case presets
OptionBuilder.build_analysis_options() # Read-only code analysis
OptionBuilder.build_chat_options() # Simple chat, no tools
OptionBuilder.quick() # Fast one-off queries
Examples
The examples/ directory contains runnable demonstrations.
Mix Task Example (Start Here)
If you want to integrate Claude into your own Mix project, see the mix_task_chat example — a complete working app with Mix tasks:
cd examples/mix_task_chat
mix deps.get
mix chat "Hello, Claude!" # Streaming response
mix chat --interactive # Multi-turn conversation
mix ask -q "What is 2+2?" # Script-friendly output
Script Examples
# Run all examples
bash examples/run_all.sh
# Run a specific example
mix run examples/basic_example.exs
mix run examples/streaming_tools/quick_demo.exs
mix run examples/hooks/basic_bash_blocking.exs
Key Examples:
-
mix_task_chat/- Full Mix task integration (streaming + interactive chat) -
basic_example.exs- Minimal SDK usage -
streaming_tools/quick_demo.exs- Real-time streaming -
hooks/complete_workflow.exs- Full hooks integration -
sdk_mcp_tools_live.exs- Custom MCP tools -
advanced_features/agents_live.exs- Multi-agent workflows -
advanced_features/subagent_spawning_live.exs- Parallel subagent coordination -
advanced_features/web_tools_live.exs- WebSearch and WebFetch
Full Application Examples
Complete Mix applications demonstrating production-ready SDK integration patterns:
| Example | Description | Key Features |
|---|---|---|
phoenix_chat/ |
Real-time chat with Phoenix LiveView | LiveView, Channels, streaming responses, session management |
document_generation/ |
AI-powered Excel document generation | elixlsx, natural language parsing, Mix tasks |
research_agent/ |
Multi-agent research coordination | Task tool, subagent tracking via hooks, parallel execution |
skill_invocation/ |
Skill tool usage and tracking | Skill definitions, hook-based tracking, GenServer state |
email_agent/ |
AI-powered email assistant | SQLite storage, rule-based processing, natural language queries |
# Run Phoenix Chat
cd examples/phoenix_chat && mix deps.get && mix phx.server
# Visit http://localhost:4000
# Run Document Generation
cd examples/document_generation && mix deps.get && mix generate.demo
# Run Research Agent
cd examples/research_agent && mix deps.get && mix research "quantum computing"
# Run Skill Invocation demo
cd examples/skill_invocation && mix deps.get && mix run -e "SkillInvocation.demo()"
# Run Email Agent
cd examples/email_agent && mix deps.get && mix email.assistant "find emails from last week"
Guides
| Guide | Description |
|---|---|
| Getting Started | Installation, authentication, and first query |
| Streaming | Real-time streaming and typewriter effects |
| Hooks | Lifecycle hooks for tool control |
| MCP Tools | In-process tool definitions with MCP |
| Permissions | Fine-grained permission controls |
| Configuration | Complete options reference |
| Agents | Custom agent personas |
| Sessions | Session management and persistence |
| Testing | Mock system and testing patterns |
| Error Handling | Error types and recovery |
Upgrading
For breaking changes and migration notes, see CHANGELOG.md.
0.9.0 breaking change (streaming):
- Stream event wrappers now require
uuidandsession_id. Missing keys raise and terminate the streaming client. - If you emit or mock
stream_eventwrappers, include both fields (custom transports, fixtures, tests).
Additional Resources:
- CHANGELOG.md - Version history and release notes
- HexDocs - API documentation
- Claude Code SDK - Upstream documentation
License
MIT License - see LICENSE for details.