Claude Agent SDK for Elixir

View Source

The idiomatic Elixir SDK for building AI agents with Claude. Native streams, in-process tools, OTP lifecycle management.

  • ✅ Full Feature Parity – 100% parity with the official Python and TypeScript SDKs
  • 📦 Zero Setup – Bundled CLI binary, auto-installed on first use. Just add the dep.
  • 🏭 OTP Native – Sessions are GenServers with standard OTP lifecycle management
  • 🔄 Elixir Streams – Native streaming with backpressure and composable pipelines
  • 🔌 In-Process Tools & Hooks – BEAM-native tools and lifecycle hooks with full access to application state
  • ⚡ Phoenix LiveView – Stream tokens directly into LiveView and PubSub

Hex.pm Documentation License Elixir

ClaudeCode
{:ok, session} = ClaudeCode.start_link()

session
|> ClaudeCode.stream("Refactor the auth module and add tests")
|> ClaudeCode.Stream.text_content()
|> Enum.each(&IO.write/1)

Why Elixir?

AI agents are long-lived processes that execute tools, maintain state, and stream responses. That's just OTP:

  • Sessions are GenServers – link to a LiveView, spawn per-request, or supervise as a service
  • Elixir Streams – backpressure, composability, and direct piping into LiveView
  • In-process tools – direct access to Ecto repos, GenServers, and caches from inside the BEAM

Install

Add to mix.exs:

def deps do
  [{:claude_code, "~> 0.21"}]
end
mix deps.get
mix claude_code.install  # optional – downloads on first use if skipped

# Authenticate (pick one)
export ANTHROPIC_API_KEY="sk-..."          # Option A: API key
"$(mix claude_code.path)" /login           # Option B: Claude subscription

Quick Start

# One-off query (ResultMessage implements String.Chars)
{:ok, result} = ClaudeCode.query("Explain GenServers in one sentence")
IO.puts(result)

# Multi-turn session with streaming
{:ok, session} = ClaudeCode.start_link()

session |> ClaudeCode.stream("My favorite language is Elixir") |> Stream.run()

session
|> ClaudeCode.stream("What's my favorite language?")
|> ClaudeCode.Stream.text_content()
|> Enum.each(&IO.write/1)
# => "Your favorite language is Elixir!"

Features

In-Process Custom Tools

Define tools that run inside your BEAM VM. They have direct access to your Ecto repos, GenServers, caches – anything in your application.

defmodule MyApp.Tools do
  use ClaudeCode.MCP.Server, name: "app-tools"

  tool :query_user, "Look up a user by email" do
    field :email, :string, required: true

    def execute(%{email: email}) do
      case MyApp.Repo.get_by(MyApp.User, email: email) do
        nil -> {:error, "User not found"}
        user -> {:ok, "#{user.name} (#{user.email})"}
      end
    end
  end
end

{:ok, result} = ClaudeCode.query("Find alice@example.com",
  mcp_servers: %{"app-tools" => MyApp.Tools},
  allowed_tools: ["mcp__app-tools__*"]
)

Pass per-session context via assigns for scoped tools in LiveView:

{:ok, session} = ClaudeCode.start_link(
  mcp_servers: %{
    "app-tools" => %{module: MyApp.Tools, assigns: %{scope: current_scope}}
  }
)

Custom tools guide → | MCP guide →

Real-Time Streaming

Native Elixir Streams with character-level deltas, composable pipelines, and direct LiveView integration:

# Character-level streaming
session
|> ClaudeCode.stream("Explain recursion", include_partial_messages: true)
|> ClaudeCode.Stream.text_deltas()
|> Enum.each(&IO.write/1)

# Phoenix LiveView
pid = self()
Task.start(fn ->
  session
  |> ClaudeCode.stream(message, include_partial_messages: true)
  |> ClaudeCode.Stream.text_deltas()
  |> Enum.each(&send(pid, {:chunk, &1}))
end)

# PubSub broadcasting
session
|> ClaudeCode.stream("Generate report", include_partial_messages: true)
|> ClaudeCode.Stream.text_deltas()
|> Enum.each(&Phoenix.PubSub.broadcast(MyApp.PubSub, "chat:#{id}", {:chunk, &1}))

Stream helpers: text_deltas/1, thinking_deltas/1, text_content/1, tool_uses/1, final_text/1, collect/1, buffered_text/1, and more.

Streaming guide →

Subagents

Define specialized agents with isolated contexts, restricted tools, and independent model selection. Claude automatically delegates tasks based on each agent's description.

alias ClaudeCode.Agent

{:ok, session} = ClaudeCode.start_link(
  agents: [
    Agent.new(
      name: "code-reviewer",
      description: "Expert code reviewer. Use for quality and security reviews.",
      prompt: "You are a code review specialist. Focus on security and best practices.",
      tools: ["Read", "Grep", "Glob"],
      model: "sonnet"
    ),
    Agent.new(
      name: "test-runner",
      description: "Runs and analyzes test suites.",
      prompt: "Run tests and provide clear analysis of results.",
      tools: ["Bash", "Read", "Grep"]
    )
  ],
  allowed_tools: ["Read", "Grep", "Glob", "Task"]
)

Subagents guide →

Hooks and Permissions

Intercept every tool execution with can_use_tool for programmatic approval, or use lifecycle hooks for auditing, budget guards, and more:

{:ok, session} = ClaudeCode.start_link(
  # Programmatic tool approval
  can_use_tool: fn %{tool_name: name}, _id ->
    if name in ["Read", "Glob", "Grep"], do: :allow, else: {:deny, "Read-only mode"}
  end,
  # Lifecycle hooks
  hooks: %{
    PostToolUse: [%{hooks: [MyApp.AuditLogger]}],
    Stop: [%{hooks: [MyApp.BudgetGuard]}]
  }
)

Six permission modes plus fine-grained tool allow/deny lists with glob patterns:

{:ok, session} = ClaudeCode.start_link(
  permission_mode: :accept_edits,
  allowed_tools: ["Read", "Edit", "Bash(git:*)"]
)

Hooks guide → | Permissions guide →

Structured Outputs

Get typed JSON data from agent workflows using JSON Schema. The agent uses tools autonomously, then returns structured results:

schema = %{
  "type" => "object",
  "properties" => %{
    "todos" => %{
      "type" => "array",
      "items" => %{
        "type" => "object",
        "properties" => %{
          "text" => %{"type" => "string"},
          "file" => %{"type" => "string"},
          "line" => %{"type" => "number"}
        },
        "required" => ["text", "file", "line"]
      }
    },
    "total_count" => %{"type" => "number"}
  },
  "required" => ["todos", "total_count"]
}

{:ok, result} = ClaudeCode.query(
  "Find all TODO comments in this codebase",
  output_format: %{type: :json_schema, schema: schema}
)

result.structured_output
# %{"todos" => [...], "total_count" => 12}

Structured outputs guide →

Session Management

Resume conversations, fork sessions, and read history:

{:ok, session} = ClaudeCode.start_link()
session |> ClaudeCode.stream("Remember: the code is 12345") |> Stream.run()

# Save session ID, stop, resume later
session_id = ClaudeCode.get_session_id(session)
ClaudeCode.stop(session)

{:ok, resumed} = ClaudeCode.start_link(resume: session_id)

# Fork a conversation into a new branch
{:ok, forked} = ClaudeCode.start_link(resume: session_id, fork_session: true)

# Runtime controls without restarting
ClaudeCode.set_model(session, "claude-sonnet-4-5-20250929")
ClaudeCode.set_permission_mode(session, :accept_edits)

Sessions guide →

Cost Controls

Track per-model usage, set budget limits, and cap turn counts:

{:ok, session} = ClaudeCode.start_link(
  max_turns: 10,
  max_budget_usd: 1.00
)

result = session
|> ClaudeCode.stream("Analyze this codebase")
|> ClaudeCode.Stream.final_result()

IO.puts("Total cost: $#{result.total_cost_usd}")

Enum.each(result.model_usage, fn {model, usage} ->
  IO.puts("#{model}: $#{usage.cost_usd} (#{usage.output_tokens} output tokens)")
end)

Cost tracking guide →

File Checkpointing

Track file changes during agent sessions and rewind to any previous state:

{:ok, session} = ClaudeCode.start_link(
  enable_file_checkpointing: true,
  permission_mode: :accept_edits
)

# Stream emits a UserMessage with a uuid before each tool execution.
# Capture it to use as a checkpoint for rewinding.
messages = session
|> ClaudeCode.stream("Refactor the authentication module")
|> Enum.to_list()

checkpoint_id =
  Enum.find_value(messages, fn
    %ClaudeCode.Message.UserMessage{uuid: uuid} when is_binary(uuid) -> uuid
    _ -> nil
  end)

# Undo all file changes back to that checkpoint
ClaudeCode.rewind_files(session, checkpoint_id)

File checkpointing guide →

MCP Integration

Connect to any MCP server – stdio, HTTP, SSE, in-process, or Hermes modules. Mix all transport types in a single session:

{:ok, session} = ClaudeCode.start_link(
  mcp_servers: %{
    "app-tools" => MyApp.Tools,                                           # In-process
    "github" => %{command: "npx", args: ["-y", "@modelcontextprotocol/server-github"],
                  env: %{"GITHUB_TOKEN" => System.get_env("GITHUB_TOKEN")}},  # stdio
    "docs" => %{type: "http", url: "https://code.claude.com/docs/mcp"}   # HTTP
  },
  allowed_tools: ["mcp__app-tools__*", "mcp__github__*", "mcp__docs__*"]
)

MCP guide →

Hosting

Every session is a GenServer wrapping a CLI subprocess. Start sessions linked to a LiveView, spawn per-request, or supervise as a named service – whatever fits your use case.

Each session maintains its own conversation context and CLI process (~50-100MB), so the typical pattern is per-user or per-request sessions rather than shared singletons. ClaudeCode.Supervisor is available for cases where you need named, long-lived sessions with automatic restart (e.g., a dedicated CI agent).

Hosting guide →

And More

  • Slash commands – Custom /commands with arguments, file references, and bash execution
  • Skills – Filesystem-based capabilities Claude invokes autonomously
  • Plugins – Package commands, agents, skills, hooks, and MCP servers for sharing
  • System prompts – Override, append, or use CLAUDE.md for project-level instructions
  • Secure deployment – Sandboxing, least-privilege tools, audit trails, and ephemeral sessions

Testing

Built-in test adapter for fast, deterministic tests without API calls:

test "handles greeting" do
  ClaudeCode.Test.stub(ClaudeCode, fn _query, _opts ->
    [ClaudeCode.Test.text("Hello! How can I help?")]
  end)

  {:ok, session} = ClaudeCode.start_link()
  result = session |> ClaudeCode.stream("Hi") |> ClaudeCode.Stream.final_text()
  assert result == "Hello! How can I help?"
end

Includes message helpers (text, tool_use, tool_result, thinking), dynamic stubs, and concurrent test support.

Testing guide →

Documentation

Contributing

We welcome contributions! Bug reports, feature requests, documentation improvements, and code contributions are all appreciated.

See our Contributing Guide to get started.

Development

git clone https://github.com/guess/claude_code.git
cd claude_code
mix deps.get
mix test
mix quality  # format, credo, dialyzer

License

MIT License – see LICENSE for details.


Built for Elixir developers on top of the Claude Code CLI.