An Elixir SDK aiming for high parity with the official claude-agent-sdk-python while treating the Claude Code CLI as the authoritative runtime contract. 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
- Common CLI query/streaming flows run on the shared
cli_subprocess_coreruntime:CliSubprocessCore.Session,CliSubprocessCore.Transport, andCliSubprocessCore.Command. ClaudeAgentSDK.Clientremains SDK-local only for the advanced Claude control family: hooks, permission callbacks, SDK MCP routing, and control request/response state.ClaudeAgentSDK.Transport.Erlexecremains available as the Claude-named public transport entrypoint backed by the shared core transport.
If you enter Claude through agent_session_manager, the normalized ASM kernel
still stops at provider selection, lane selection, event projection, and
session/run orchestration.
The optional ASM seam for Claude lives under
ASM.Extensions.ProviderSDK.Claude. That extension may:
- derive
ClaudeAgentSDK.Optionsfrom ASM-style config - start
ClaudeAgentSDK.Clientfrom ASM session defaults or ASM-style config
If claude_agent_sdk is present in the dependency graph, ASM activates that
namespace automatically in ASM.Extensions.ProviderSDK.available_extensions/0
and ASM.Extensions.ProviderSDK.capability_report/0. Client apps do not need
to register it manually.
ASM-owned fields such as cwd, permission_mode, model, max_turns, and
the transport timeout still stay in ASM config. The extension's native override
bag is for Claude-native fields only.
It does not move the control family into ASM. Once you cross that seam, the real control APIs remain here:
ClaudeAgentSDK.Client.set_permission_mode/2ClaudeAgentSDK.Client.set_model/2ClaudeAgentSDK.Client.interrupt/1ClaudeAgentSDK.Client.rewind_files/2ClaudeAgentSDK.ControlProtocol.Protocol
Phase 4 finalizes the Claude release boundary:
cli_subprocess_coreremains the required lower dependency for the common Claude query and streaming laneclaude_agent_sdkremains the source of truth for Claude-native control features such as hooks, permission callbacks, SDK MCP routing, and control protocol state- ASM may bridge into that richer family only through
ASM.Extensions.ProviderSDK.Claude; that seam does not move the control family into ASM or the shared core - the operator publication order remains
cli_subprocess_core, thenclaude_agent_sdk, thenagent_session_manager
Add to your mix.exs:
def deps do
[
{:claude_agent_sdk, "~> 0.16.0"}
]
endThen fetch dependencies:
mix deps.getInstall the Claude Code CLI (requires Node.js):
npm install -g @anthropic-ai/claude-codeVerify installation:
claude --version- Minimum supported Claude CLI version:
2.1.0 - Recommended Claude CLI version:
2.1.74 - Compatibility policy: this SDK follows the Python SDK where practical, but the Claude CLI wire protocol is authoritative. CLI-native frames such as
:rate_limit_eventare surfaced here even if the current Python SDK skips unknown message types for forward compatibility.
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 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, {CliSubprocessCore.Transport, []})
|> Enum.to_list()
# Lazy transport startup (defer subprocess spawn to handle_continue)
ClaudeAgentSDK.query(
"Hello",
opts,
{CliSubprocessCore.Transport, [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 Agent tool, events include a parent_tool_use_id field to identify the source. Main agent events have nil, subagent events have the Agent 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 | :auto | :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, :plan, or the CLI's :auto mode for built-in tool permissions. Hook-based fallback only applies when the CLI does not emit can_use_tool, and that fallback ignores updated_permissions. :delegate is no longer forwarded because current Claude CLI builds reject it.
CLI transcript history now matches the official SDK surface:
# Claude CLI transcript history
sessions = ClaudeAgentSDK.list_sessions(directory: "/path/to/project")
messages = ClaudeAgentSDK.get_session_messages("550e8400-e29b-41d4-a716-446655440000",
directory: "/path/to/project"
)
# SDK-managed SessionStore history
{:ok, saved_sessions} = ClaudeAgentSDK.list_saved_sessions(storage_dir: "/custom/path")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]
)
# Without :supervisor, the SDK keeps the registry under its internal
# SDK MCP supervisor so the server survives creator process exits.
# 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" |
effort |
atom | :low, :medium, :high, :max — controls reasoning effort; :max is Opus-only; invalid values raise ArgumentError (not supported for Haiku) |
thinking |
map | %{type: :adaptive}, %{type: :enabled, budget_tokens: N}, %{type: :disabled} |
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, :auto, :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) |
transport_error_mode |
atom | :result (default) or :raise for strict transport/decode failures |
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).
All tunable constants (timeouts, buffer sizes, auth paths, CLI flags, env var
names, concurrency limits) are centralized in Config.* sub-modules and can
be overridden per-environment:
# config/config.exs
config :claude_agent_sdk, ClaudeAgentSDK.Config.Timeouts,
query_total_ms: 5_400_000, # total query timeout (default: 75 min)
tool_execution_ms: 60_000 # per-tool timeout (default: 30 s)
config :claude_agent_sdk, ClaudeAgentSDK.Config.Buffers,
max_stdout_buffer_bytes: 2_097_152 # stdout buffer (default: 1 MB)
config :claude_agent_sdk, ClaudeAgentSDK.Config.Orchestration,
max_concurrent: 10, # parallel query limit (default: 5)
max_retries: 5 # retry attempts (default: 3)
# Legacy flat keys still work:
config :claude_agent_sdk,
cli_stream_module: ClaudeAgentSDK.Query.CLIStream,
task_supervisor_strict: falseSee the Configuration Internals guide for the complete reference of every tunable, its default, and override examples.
config :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.
CliSubprocessCore.Transport, Transport.Erlexec, and Streaming.Session
support startup_mode: :lazy
to defer subprocess startup to handle_continue/2. Deterministic startup
validation still happens before start_link returns, so missing cwd/command
style failures surface immediately. Once preflight passes, lazy mode can still
surface subprocess launch failures as a process exit after init.
Query-side transport errors normalize equivalent reasons to stable atoms where possible:
{: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 queries
# Effort and thinking helpers
OptionBuilder.with_opus()
|> OptionBuilder.with_effort(:max)
|> OptionBuilder.with_thinking(%{type: :adaptive})The 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 toolsmax_effort_opus_live.exs- Opus:maxeffort (not inrun_all.sh)advanced_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 | Agent 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.12.0 breaking changes:
Transport.Portremoved. The built-in common transport lane now runs throughCliSubprocessCore.Transport;Transport.Erlexecremains available as the Claude-named public transport entrypoint.Transport.normalize_reason(:port_closed)removed. Custom transports should return:not_connecteddirectly.- Transport error tuple shape updated: low-level failures now use
{:error, {:transport, reason}}instead of bare{:error, reason}. - String prompts now delivered via stdin (
--input-format stream-json) instead of CLI arg (-- prompt).
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.