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).
- 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
Add to your mix.exs:
def deps do
[
{:claude_agent_sdk, "~> 0.11.0"}
]
endThen fetch dependencies:
mix deps.getInstall the Claude Code CLI (requires Node.js):
npm install -g @anthropic-ai/claude-codeVerify installation:
claude --versionChoose 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 loginalias 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)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)If session initialization or message send fails, the stream now emits an immediate
%{type: :error, error: reason} event instead of waiting for the 5-minute stream timeout.
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 |
AWS Bedrock:
export CLAUDE_AGENT_USE_BEDROCK=1
export AWS_ACCESS_KEY_ID=...
export AWS_SECRET_ACCESS_KEY=...
export AWS_REGION=us-west-2Google Vertex AI:
export CLAUDE_AGENT_USE_VERTEX=1
export GOOGLE_APPLICATION_CREDENTIALS=/path/to/key.json
export GOOGLE_CLOUD_PROJECT=your-project-idFor persistent authentication without re-login:
mix claude.setup_tokenAuthManager keeps running if token storage save/clear fails and returns {:error, reason}.
Handle clear_auth/0 accordingly in your app code:
case ClaudeAgentSDK.AuthManager.clear_auth() do
:ok -> :ok
{:error, reason} -> IO.puts("Failed to clear auth: #{inspect(reason)}")
endCheck authentication status:
alias ClaudeAgentSDK.AuthChecker
diagnosis = AuthChecker.diagnose()
# => %{authenticated: true, auth_method: "Anthropic 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 |
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()
# Lazy transport startup (defer subprocess spawn to handle_continue)
ClaudeAgentSDK.query(
"Hello",
opts,
{ClaudeAgentSDK.Transport.Port, [startup_mode: :lazy]}
)
|> 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()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.
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 (all 12 Python SDK events supported):
pre_tool_use/post_tool_use/post_tool_use_failure- Tool execution lifecycleuser_prompt_submit- Before sending user messagesstop/subagent_stop/subagent_start- Agent lifecyclenotification- CLI notificationspermission_request- Permission dialog interceptionsession_start/session_end- Session lifecyclepre_compact- Before context compaction
See the Hooks Guide for comprehensive documentation.
Hook and permission callbacks run in async tasks. For production, add the SDK task supervisor so callback processes are supervised:
children = [
ClaudeAgentSDK.TaskSupervisor,
{ClaudeAgentSDK.Client, options}
]If you use a custom supervisor name, configure the SDK to match:
children = [
{ClaudeAgentSDK.TaskSupervisor, name: MyApp.ClaudeTaskSupervisor}
]
config :claude_agent_sdk, task_supervisor: MyApp.ClaudeTaskSupervisorIf an explicitly configured supervisor is missing at runtime, the SDK logs a warning and
falls back to Task.start/1. With default settings, missing
ClaudeAgentSDK.TaskSupervisor falls back silently for backward compatibility.
For stricter behavior in dev/test:
config :claude_agent_sdk, task_supervisor_strict: trueIn strict mode, ClaudeAgentSDK.TaskSupervisor.start_child/2 returns
{:error, {:task_supervisor_unavailable, supervisor}} instead of spawning
an unsupervised fallback task.
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()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]
)
# Optional: start tool registry under your DynamicSupervisor
{:ok, sup} = DynamicSupervisor.start_link(strategy: :one_for_one)
server = ClaudeAgentSDK.create_sdk_mcp_server(
name: "calculator",
version: "1.0.0",
tools: [MyTools.Calculate],
supervisor: sup
)
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".
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).
config :claude_agent_sdk,
# Timeout for in-process tool execution tasks in Tool.Registry
tool_execution_timeout_ms: 30_000,
# Query CLI stream backend module
cli_stream_module: ClaudeAgentSDK.Query.CLIStream,
# Fail fast when configured task supervisor is unavailable
task_supervisor_strict: falseconfig :claude_agent_sdk, :process_module is still read as a fallback for query streaming,
but it is deprecated and logs a warning once per legacy module.
SessionStore now hydrates on-disk cache in a handle_continue/2 step. Startup is faster,
but list/search can be briefly incomplete immediately after boot while warmup finishes.
Transport.Port, Transport.Erlexec, and Streaming.Session support startup_mode: :lazy
to defer subprocess startup to handle_continue/2. In lazy mode, start_link can succeed
before the subprocess is spawned; startup failures then surface as process exit after init.
Query-side transport errors normalize equivalent reasons to stable atoms where possible:
:port_closed is treated as :not_connected, and {:command_not_found, "claude"}
is treated as :cli_not_found.
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 | :offalias 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 queriesThe examples/ directory contains runnable demonstrations.
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# 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.exsKey Examples:
mix_task_chat/- Full Mix task integration (streaming + interactive chat)basic_example.exs- Minimal SDK usagestreaming_tools/quick_demo.exs- Real-time streaminghooks/complete_workflow.exs- Full hooks integrationsdk_mcp_tools_live.exs- Custom MCP toolsadvanced_features/agents_live.exs- Multi-agent workflowsadvanced_features/subagent_spawning_live.exs- Parallel subagent coordinationadvanced_features/web_tools_live.exs- WebSearch and WebFetch
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"| 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 |
For breaking changes and migration notes, see CHANGELOG.md.
0.11.0 breaking changes:
--printflag removed from all modules. All queries now use--output-format stream-jsonexclusively.--agentsCLI flag removed. Agents are now sent via theinitializecontrol request. UseOptions.agents_for_initialize/1.AgentsFilemodule deleted. Remove anyagents_temp_file_max_age_secondsconfig.Clientstate is now adefstruct. Four deprecated fields removed:current_model,pending_model_change,current_permission_mode,pending_inbound_count.- All 12 hook events are now supported (6 new:
post_tool_use_failure,notification,subagent_start,permission_request,session_start,session_end).
0.10.0 fix (resume turn persistence):
resume/3no longer uses--print --resume(one-shot mode that dropped intermediate turns). It now uses--resumewith--input-format stream-json, preserving the full conversation history across resume calls.- Updated default Opus model to
claude-opus-4-6.
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
MIT License - see LICENSE for details.