Skip to content

An Elixir Claude Agent SDK for building AI agents with Claude Code

License

Notifications You must be signed in to change notification settings

guess/claude_code

Repository files navigation

Claude Agent SDK for Elixir

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.20"}]
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 [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 →

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.

About

An Elixir Claude Agent SDK for building AI agents with Claude Code

Topics

Resources

License

Contributing

Stars

Watchers

Forks

Packages

No packages published

Contributors 4

  •  
  •  
  •  
  •