README
View SourceClaude Agent SDK for Elixir
claude_agent_sdk is an Elixir SDK for programmatically interacting with Claude Code via the Claude Code CLI. It provides:
- A clean, streaming-first API (
ClaudeAgentSDK.query/2,ClaudeAgentSDK.Streaming) - A full bidirectional control client for advanced features (hooks, permissions, SDK MCP tooling)
- Operational tooling (auth diagnostics, debug mode, orchestration, session persistence)
Quick links
- HexDocs: https://hexdocs.pm/claude_agent_sdk
- Hex package: https://hex.pm/packages/claude_agent_sdk
- Claude Code SDK docs (upstream): https://docs.anthropic.com/en/docs/claude-code/sdk
- Claude Code hooks (upstream): https://docs.anthropic.com/en/docs/claude-code/hooks
Architecture
The SDK has two βlanesβ:
- CLI-only lane (fast path): simple queries and pure streaming
- Control-client lane (feature path): hooks, permissions, SDK MCP servers, runtime control
flowchart TB
App[Your Elixir app] --> SDK[ClaudeAgentSDK API]
SDK -->|"query/continue/resume"| Query[ClaudeAgentSDK.Query]
SDK -->|"start_session/send_message"| StreamAPI[ClaudeAgentSDK.Streaming]
Query --> Router["StreamingRouter / feature detection"]
StreamAPI --> Router
Router -->|"CLI-only path"| Proc["Process / Streaming.Session"]
Router -->|"Control path"| Client["Client (GenServer + control protocol)"]
Client --> Transport["Transport (Port or Erlexec)"]
Proc --> CLI["Claude Code CLI"]
Transport --> CLI
CLI --> Claude["Claude service"]
Client --> Hooks["Hooks callbacks"]
Client --> Perms["Permission callback"]
Client --> MCP["SDK MCP bridge"]Prerequisites
Install Claude Code CLI
This library shells out to the Claude Code CLI. Install it first:
npm install -g @anthropic-ai/claude-code
CLI discovery and version checks
The SDK centralizes CLI discovery in ClaudeAgentSDK.CLI:
- Candidate executables:
claude-code, thenclaude - Minimum supported version:
2.0.0 - Recommended version:
2.0.75
You can verify what the SDK sees:
{:ok, path} = ClaudeAgentSDK.CLI.find_executable()
{:ok, version} = ClaudeAgentSDK.CLI.version()
ClaudeAgentSDK.CLI.version_supported?()
ClaudeAgentSDK.CLI.warn_if_outdated()Installation
Add the dependency to mix.exs:
def deps do
[
{:claude_agent_sdk, "~> 0.6.9"}
]
endThen:
mix deps.get
Authentication
The SDK supports three approaches (in precedence order):
- Environment variable credentials (best for CI/CD)
- Stored OAuth token via AuthManager (best for local dev without re-login)
- Existing
claude loginsession (legacy/manual)
Recommended for CI/CD: environment variables
Anthropic:
export CLAUDE_AGENT_OAUTH_TOKEN="sk-ant-oat01-..."
# or legacy:
export ANTHROPIC_API_KEY="sk-ant-api03-..."
AWS Bedrock:
export CLAUDE_AGENT_USE_BEDROCK=1
export AWS_ACCESS_KEY_ID=...
export AWS_SECRET_ACCESS_KEY=...
export AWS_REGION=us-west-2
# or AWS_PROFILE / ~/.aws/credentials
Google Vertex AI:
export CLAUDE_AGENT_USE_VERTEX=1
export GOOGLE_APPLICATION_CREDENTIALS=/path/to/key.json
export GOOGLE_CLOUD_PROJECT=your-project-id
Local dev: one-time OAuth token setup
The SDK includes a Mix task that runs claude setup-token and persists the token securely:
mix claude.setup_token
Status and health checks:
alias ClaudeAgentSDK.AuthManager
:ok = AuthManager.ensure_authenticated()
status = AuthManager.status()Diagnostics
If you want a clear, actionable environment report:
alias ClaudeAgentSDK.AuthChecker
diagnosis = AuthChecker.diagnose()
AuthChecker.ensure_ready!()Core API
The SDK provides three main APIs. Choose based on your use case:
| API | Use Case | Tool Support | Real-time Output |
|---|---|---|---|
query/2 | Simple queries, batch processing | Yes | No (buffers) |
Streaming | Typewriter UX, chat interfaces | Limited | Yes (text_delta) |
Client | Multi-turn agents with tools | Full | Yes (per-message) |
Available Models
Set via %Options{model: "..."}. When nil, uses CLI default.
"sonnet"- Claude Sonnet (recommended for coding tasks)"opus"- Claude Opus (most capable)"haiku"- Claude Haiku (fastest, cheapest)
Message Types
When processing messages, you'll see these types:
:system- Session initialization (subtype::init):assistant- Claude's text responses:tool_use- Claude requesting a tool (data includes:toolname):tool_result- Result from tool execution:result- Final result (subtype::successor error type)
ClaudeAgentSDK.query/2
query/2 returns a lazy stream of ClaudeAgentSDK.Message structs. Best for simple queries or when you don't need real-time output.
alias ClaudeAgentSDK.{ContentExtractor, Options}
opts = %Options{max_turns: 3, output_format: :stream_json}
ClaudeAgentSDK.query("Say hello from Elixir", opts)
|> Enum.each(fn msg ->
case msg.type do
:assistant ->
IO.puts(ContentExtractor.extract_text(msg) || "")
:result ->
IO.inspect(msg.data, label: "result")
:ok
_ ->
:ok
end
end)Note: For multi-turn tool use with real-time output, use
Clientinstead (see below).
continue/2 and resume/3
# Continue the last conversation
ClaudeAgentSDK.continue("Add error handling")
|> Enum.to_list()
# Resume a specific session
ClaudeAgentSDK.resume("session-id", "Now add tests")
|> Enum.to_list()Options: configuring behavior
ClaudeAgentSDK.Options maps directly to CLI flags (plus higher-level SDK routing):
Common fields you will likely use:
max_turnssystem_prompt/append_system_promptoutput_format(:text | :json | :stream_json | %{type: :json_schema, schema: ...})model/fallback_modelallowed_tools/disallowed_toolspermission_modecwdtimeout_msinclude_partial_messages(streaming)preferred_transport(:auto | :cli | :control)
Example:
alias ClaudeAgentSDK.Options
opts = %Options{
model: "sonnet",
fallback_model: "haiku",
max_turns: 5,
permission_mode: :plan,
output_format: :stream_json,
allowed_tools: ["Read", "Grep"],
cwd: "/path/to/project"
}Option presets with OptionBuilder
If you want sensible defaults per environment / use case:
alias ClaudeAgentSDK.OptionBuilder
dev_opts = OptionBuilder.build_development_options()
prod_opts = OptionBuilder.build_production_options()
analysis = OptionBuilder.build_analysis_options()
env_opts = OptionBuilder.for_environment()Streaming (typewriter / incremental UX)
ClaudeAgentSDK.Streaming provides persistent sessions and text_delta events.
alias ClaudeAgentSDK.Streaming
{:ok, session} = Streaming.start_session()
Streaming.send_message(session, "Write a one-sentence summary of OTP.")
|> Stream.each(fn
%{type: :text_delta, text: chunk} -> IO.write(chunk)
%{type: :message_stop} -> IO.puts("\n")
_ -> :ok
end)
|> Stream.run()
:ok = Streaming.close_session(session)Automatic transport selection
Streaming uses ClaudeAgentSDK.Transport.StreamingRouter to select:
- CLI streaming session when you do not require control features
- Control client when you enable hooks, permissions, SDK MCP servers, or certain runtime agent/permission settings
You can override selection:
alias ClaudeAgentSDK.Options
# Force CLI-only
opts = %Options{preferred_transport: :cli}
# Force control client
opts = %Options{preferred_transport: :control}Control-client features
Use ClaudeAgentSDK.Client when you need:
- Multi-turn tool use with real-time output (recommended for agents)
- Hooks, permission gating, SDK MCP tools
- Runtime model switching or bidirectional control
Multi-turn agent with real-time output
This is the recommended pattern for running an agent that uses tools and shows each step as it happens:
alias ClaudeAgentSDK.{Client, ContentExtractor, Message, Options}
options = %Options{model: "sonnet", cwd: "/path/to/project"}
{:ok, client} = Client.start_link(options)
# Start streaming in a Task (messages arrive as agent works)
task = Task.async(fn ->
Client.stream_messages(client)
|> Enum.reduce_while(:ok, fn message, acc ->
case message do
%Message{type: :assistant} = msg ->
text = ContentExtractor.extract_text(msg)
if text && text != "", do: IO.puts(text)
{:cont, acc}
%Message{type: :tool_use} = msg ->
tool = msg.data[:tool] || "unknown"
IO.puts("π§ Using: #{tool}")
{:cont, acc}
%Message{type: :tool_result} ->
IO.puts("π Tool completed")
{:cont, acc}
%Message{type: :result, subtype: :success} ->
IO.puts("β
Done")
{:halt, :ok}
%Message{type: :result, subtype: subtype} ->
IO.puts("β Failed: #{subtype}")
{:halt, {:error, subtype}}
_ ->
{:cont, acc}
end
end)
end)
# Send the prompt
:ok = Client.send_message(client, "Read the README and summarize it.")
# Wait for completion (10 minute timeout)
result = Task.await(task, 600_000)
Client.stop(client)Simple client lifecycle
For simpler cases where you don't need real-time output:
alias ClaudeAgentSDK.{Client, Options}
{:ok, client} = Client.start_link(%Options{model: "sonnet"})
:ok = Client.send_message(client, "Summarize this repository in 3 bullets.")
Client.stream_messages(client)
|> Enum.take_while(&(&1.type != :result))
|> Enum.each(&IO.inspect/1)
:ok = Client.stop(client)Hooks
Hooks are callback functions invoked by the Claude Code CLI during agent execution (tool calls, prompt submission, lifecycle events). They are configured through Options.hooks using matchers.
alias ClaudeAgentSDK.{Client, Options}
alias ClaudeAgentSDK.Hooks.{Matcher, Output}
defmodule MyHooks do
def block_dangerous_bash(%{"tool_name" => "Bash", "tool_input" => %{"command" => cmd}}, _id, _ctx) do
if String.contains?(cmd, "rm -rf") do
Output.deny("Blocked potentially destructive command")
|> Output.with_system_message("Command blocked by policy.")
else
Output.allow()
end
end
def block_dangerous_bash(_input, _id, _ctx), do: %{}
end
opts = %Options{
hooks: %{
pre_tool_use: [
Matcher.new("Bash", [&MyHooks.block_dangerous_bash/3], timeout_ms: 1_500)
]
}
}
{:ok, client} = Client.start_link(opts)
:ok = Client.send_message(client, "Try running rm -rf /tmp (do not actually do it).")
Client.stream_messages(client) |> Enum.to_list()
Client.stop(client)Supported hook events (see ClaudeAgentSDK.Hooks):
:session_start,:session_end:notification:pre_tool_use,:post_tool_use:user_prompt_submit:stop,:subagent_stop:pre_compact
Permission system
The permission system allows you to centrally control tool execution with a callback.
- Callback:
ClaudeAgentSDK.Permission.callback/0 - Context:
ClaudeAgentSDK.Permission.Context - Result:
ClaudeAgentSDK.Permission.Result Modes:
:default | :accept_edits | :plan | :bypass_permissions
alias ClaudeAgentSDK.{Client, Options}
alias ClaudeAgentSDK.Permission.Result
permission_callback = fn ctx ->
case {ctx.tool_name, ctx.tool_input} do
{"Bash", %{"command" => cmd}} when is_binary(cmd) and String.contains?(cmd, "rm -rf") ->
Result.deny("Command blocked by policy", interrupt: true)
_ ->
Result.allow()
end
end
opts = %Options{
permission_mode: :default,
can_use_tool: permission_callback
}
{:ok, client} = Client.start_link(opts)
:ok = Client.send_message(client, "Try a bash command.")
Client.stream_messages(client) |> Enum.to_list()
Client.stop(client)Runtime mode switching is supported:
:ok = ClaudeAgentSDK.Client.set_permission_mode(client, :plan)Agents (custom personas)
Agents are first-class structs (ClaudeAgentSDK.Agent) you can attach to options and switch at runtime.
alias ClaudeAgentSDK.{Agent, Client, Options}
coder =
Agent.new(
name: :coder,
description: "Implementation-focused engineering assistant",
prompt: "You are a pragmatic senior engineer. Prefer small, safe changes.",
allowed_tools: ["Read", "Write", "Grep"],
model: "sonnet"
)
reviewer =
Agent.new(
name: :reviewer,
description: "Strict code reviewer",
prompt: "You review code for correctness, safety, and clarity. Be precise.",
allowed_tools: ["Read", "Grep"],
model: "sonnet"
)
opts = %Options{agents: %{coder: coder, reviewer: reviewer}, agent: :reviewer}
{:ok, client} = Client.start_link(opts)
:ok = Client.set_agent(client, :coder)
{:ok, active} = Client.get_agent(client)
Client.stop(client)SDK MCP servers (in-process tools)
The SDK includes a lightweight in-process MCP tool system:
- Define tools via
use ClaudeAgentSDK.Tool+deftool - Create a server via
ClaudeAgentSDK.create_sdk_mcp_server/1 - Provide it via
Options.mcp_servers
Note: SDK MCP support depends on CLI control-protocol messages; the SDK implements routing and JSON-RPC responses, but availability depends on your Claude Code CLI version and feature set.
defmodule MyTools do
use ClaudeAgentSDK.Tool
deftool :add, "Add two numbers", %{
type: "object",
properties: %{
a: %{type: "number"},
b: %{type: "number"}
},
required: ["a", "b"]
} do
def execute(%{"a" => a, "b" => b}) do
{:ok, %{"content" => [%{"type" => "text", "text" => "#{a + b}"}]}}
end
end
end
server =
ClaudeAgentSDK.create_sdk_mcp_server(
name: "calculator",
version: "1.0.0",
tools: [MyTools.Add]
)
opts = %ClaudeAgentSDK.Options{
mcp_servers: %{"calculator" => server}
}Orchestration (parallelism, pipelines, retry)
For application-level workflows:
alias ClaudeAgentSDK.Orchestrator
queries = [
{"Analyze module A", %ClaudeAgentSDK.Options{max_turns: 3}},
{"Analyze module B", %ClaudeAgentSDK.Options{max_turns: 3}}
]
{:ok, results} = Orchestrator.query_parallel(queries, max_concurrent: 2)
{:ok, final} =
Orchestrator.query_pipeline(
[
{"Summarize this code", %ClaudeAgentSDK.Options{}},
{"Suggest refactors", %ClaudeAgentSDK.Options{}}
],
use_context: true
)
{:ok, messages} =
Orchestrator.query_with_retry(
"Do a quick review",
%ClaudeAgentSDK.Options{},
max_retries: 3,
backoff_ms: 1_000
)Session persistence
ClaudeAgentSDK.SessionStore persists message histories and metadata.
alias ClaudeAgentSDK.{Session, SessionStore}
{:ok, _} = SessionStore.start_link()
messages = ClaudeAgentSDK.query("Draft an implementation plan") |> Enum.to_list()
session_id = Session.extract_session_id(messages)
:ok =
SessionStore.save_session(session_id, messages,
tags: ["planning", "important"],
description: "Implementation plan draft"
)
{:ok, session_data} = SessionStore.load_session(session_id)
sessions = SessionStore.search(tags: ["important"])Debugging and diagnostics
DebugMode
alias ClaudeAgentSDK.DebugMode
DebugMode.run_diagnostics()
messages = DebugMode.debug_query("Explain supervision trees")
stats = DebugMode.analyze_messages(messages)
bench = DebugMode.benchmark("hello", nil, 3)Content extraction helper
alias ClaudeAgentSDK.ContentExtractor
text =
ClaudeAgentSDK.query("Write a haiku about BEAM")
|> Stream.filter(&ContentExtractor.has_text?/1)
|> Stream.map(&ContentExtractor.extract_text/1)
|> Enum.join("\n")Mix tasks (included)
This repository ships operational Mix tasks you can run directly:
mix claude.setup_tokenInteractive OAuth token acquisition + persistence (usesclaude setup-token)mix showcase [--live]Run a comprehensive feature demo in mock mode (default) or live modemix run.live path/to/script.exs [args...]Runs scripts with mocking disabled (live API calls)mix test.live [mix test args...]Runs tests with mocking disabled; defaults to--only liveunless overridden
Security and operational guidance
- Prefer
permission_mode: :planor explicitallowed_toolsin production workloads. - Treat tokens as secrets; the default token store writes
~/.claude_sdk/token.jsonwith user-only permissions. - Use hooks and/or the permission callback to centrally enforce policy (file access rules, command allow-lists, audit logging).
- When running live in CI, use environment variables and avoid interactive flows.
Repository reference links
If you are browsing this repository, these files are the best entry points:
- Core public API:
claude_agent_sdk.ex - Options and CLI flag mapping:
claude_agent_sdk/options.ex - Query routing:
claude_agent_sdk/query.ex - Streaming API and session backend:
claude_agent_sdk/streaming.ex,claude_agent_sdk/streaming/session.ex - Control client (hooks, permissions, MCP):
claude_agent_sdk/client.ex - Hooks system:
claude_agent_sdk/hooks/* - Permission system:
claude_agent_sdk/permission/* - Auth tooling:
claude_agent_sdk/auth_manager.ex,claude_agent_sdk/auth_checker.ex,claude_agent_sdk/auth/* - Orchestration:
claude_agent_sdk/orchestrator.ex - Persistence:
claude_agent_sdk/session_store.ex - Diagnostics:
claude_agent_sdk/debug_mode.ex
License
MIT License