Claude Agent SDK Logo

Claude Agent SDK for Elixir

Hex.pm Hex Docs Hex.pm Downloads License CI Last Commit

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.2"}
  ]
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:

MethodEnvironment VariableBest For
OAuth TokenCLAUDE_AGENT_OAUTH_TOKENProduction / CI
API KeyANTHROPIC_API_KEYDevelopment
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

APIUse CaseWhen to Use
query/2Simple queriesBatch processing, scripts
StreamingTypewriter UXChat interfaces, real-time output
ClientFull controlMulti-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.

Supervision

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.ClaudeTaskSupervisor

If the configured supervisor is missing at runtime, the SDK logs a warning and falls back to Task.start/1. For stricter behavior in dev/test:

config :claude_agent_sdk, task_supervisor_strict: true

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:

OptionTypeDescription
modelstring"sonnet", "opus", "haiku"
max_turnsintegerMaximum conversation turns
system_promptstringCustom system instructions
output_formatatom/map:text, :json, :stream_json, or JSON schema (SDK enforces stream-json for transport; JSON schema still passed)
allowed_toolslistTools Claude can use
permission_modeatom:default, :accept_edits, :plan, :bypass_permissions, :delegate, :dont_ask
hooksmapLifecycle hook callbacks
mcp_serversmap or stringMCP server configurations (or JSON/path alias for mcp_config)
cwdstringWorking directory for file operations
timeout_msintegerCommand timeout (default: 75 minutes)
max_buffer_sizeintegerMaximum 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:

ExampleDescriptionKey Features
phoenix_chat/Real-time chat with Phoenix LiveViewLiveView, Channels, streaming responses, session management
document_generation/AI-powered Excel document generationelixlsx, natural language parsing, Mix tasks
research_agent/Multi-agent research coordinationTask tool, subagent tracking via hooks, parallel execution
skill_invocation/Skill tool usage and trackingSkill definitions, hook-based tracking, GenServer state
email_agent/AI-powered email assistantSQLite 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

GuideDescription
Getting StartedInstallation, authentication, and first query
StreamingReal-time streaming and typewriter effects
HooksLifecycle hooks for tool control
MCP ToolsIn-process tool definitions with MCP
PermissionsFine-grained permission controls
ConfigurationComplete options reference
AgentsCustom agent personas
SessionsSession management and persistence
TestingMock system and testing patterns
Error HandlingError types and recovery

Upgrading

For breaking changes and migration notes, see CHANGELOG.md.

0.9.0 breaking change (streaming):

  • Stream event wrappers now require uuid and session_id. Missing keys raise and terminate the streaming client.
  • If you emit or mock stream_event wrappers, include both fields (custom transports, fixtures, tests).

Additional Resources:


License

MIT License - see LICENSE for details.


Built with Elixir and Claude