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
{:ok, session} = ClaudeCode.start_link()
session
|> ClaudeCode.stream("Refactor the auth module and add tests")
|> ClaudeCode.Stream.text_content()
|> Enum.each(&IO.write/1)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
Add to mix.exs:
def deps do
[{:claude_code, "~> 0.20"}]
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# 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!"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 [email protected]",
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 →
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.
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"]
)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 →
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}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)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)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)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__*"]
)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).
- 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
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 Hub – All guides and references
- API Reference – Complete API docs on HexDocs
- Examples – Real-world usage patterns
- Troubleshooting – Common issues and solutions
We welcome contributions! Bug reports, feature requests, documentation improvements, and code contributions are all appreciated.
See our Contributing Guide to get started.
git clone https://github.com/guess/claude_code.git
cd claude_code
mix deps.get
mix test
mix quality # format, credo, dialyzerMIT License – see LICENSE for details.
Built for Elixir developers on top of the Claude Code CLI.
