Claude Agent SDK Logo

Claude Agent SDK for Elixir

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

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)

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, then claude
  • 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"}
  ]
end

Then:

mix deps.get

Authentication

The SDK supports three approaches (in precedence order):

  1. Environment variable credentials (best for CI/CD)
  2. Stored OAuth token via AuthManager (best for local dev without re-login)
  3. Existing claude login session (legacy/manual)

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:

APIUse CaseTool SupportReal-time Output
query/2Simple queries, batch processingYesNo (buffers)
StreamingTypewriter UX, chat interfacesLimitedYes (text_delta)
ClientMulti-turn agents with toolsFullYes (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 :tool name)
  • :tool_result - Result from tool execution
  • :result - Final result (subtype: :success or 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 Client instead (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_turns
  • system_prompt / append_system_prompt
  • output_format (:text | :json | :stream_json | %{type: :json_schema, schema: ...})

  • model / fallback_model
  • allowed_tools / disallowed_tools
  • permission_mode
  • cwd
  • timeout_ms
  • include_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.

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:

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_token Interactive OAuth token acquisition + persistence (uses claude setup-token)

  • mix showcase [--live] Run a comprehensive feature demo in mock mode (default) or live mode

  • mix 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 live unless overridden


Security and operational guidance

  • Prefer permission_mode: :plan or explicit allowed_tools in production workloads.
  • Treat tokens as secrets; the default token store writes ~/.claude_sdk/token.json with 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.

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