Claude Agent SDK for Elixir
View SourceThe 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

{: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"}]
endmix 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.
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"]
)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}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)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)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)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__*"]
)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).
And More
- Slash commands – Custom
/commandswith 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?"
endIncludes message helpers (text, tool_use, tool_result, thinking), dynamic stubs, and concurrent test support.
Documentation
- Documentation Hub – All guides and references
- API Reference – Complete API docs on HexDocs
- Examples – Real-world usage patterns
- Troubleshooting – Common issues and solutions
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.