ClaudeCode
View SourceAn idiomatic Elixir SDK for Claude Code - bringing AI-powered coding assistance to the Elixir ecosystem.
ClaudeCode provides a GenServer-based interface to the Claude Code CLI with support for streaming responses, concurrent queries, and Phoenix LiveView integration.
Prerequisites
Install Claude Code CLI:
- Visit claude.ai/code
- Follow the installation instructions for your platform
- Verify installation:
claude --version
Get an API Key:
- Sign up at console.anthropic.com
- Create an API key and set it as an environment variable:
export ANTHROPIC_API_KEY="sk-ant-..."
Installation
Add to your mix.exs
:
def deps do
[
{:claude_code, "~> 0.1.0"}
]
end
Then run:
mix deps.get
Quick Start
Basic Usage
# Start a session
{:ok, session} = ClaudeCode.start_link(
api_key: System.get_env("ANTHROPIC_API_KEY")
)
# Send a query and get a response
{:ok, response} = ClaudeCode.query_sync(session, "Hello, Claude!")
IO.puts(response)
# => "Hello! How can I assist you today?"
# Stop the session when done
ClaudeCode.stop(session)
Configuration
Configure sessions with various options:
# Session with custom configuration
{:ok, session} = ClaudeCode.start_link(
api_key: System.get_env("ANTHROPIC_API_KEY"),
model: "opus",
system_prompt: "You are an Elixir expert",
allowed_tools: ["View", "Edit", "Bash(git:*)"],
add_dir: ["/tmp", "/var/log"],
timeout: 120_000,
permission_mode: :default
)
# Use application configuration for defaults
# config/config.exs
config :claude_code,
model: "opus",
timeout: 180_000,
system_prompt: "You are a helpful Elixir assistant",
allowed_tools: ["View", "Edit", "Bash(git:*)"],
add_dir: ["/tmp"]
# Session automatically uses configured defaults
{:ok, session} = ClaudeCode.start_link(api_key: "sk-ant-...")
Streaming Responses
Process responses as they arrive:
# Stream text content
session
|> ClaudeCode.query("Explain GenServers in Elixir")
|> ClaudeCode.Stream.text_content()
|> Enum.each(&IO.write/1)
# React to tool usage in real-time
session
|> ClaudeCode.query("Create a new Elixir module")
|> ClaudeCode.Stream.tool_uses()
|> Enum.each(fn tool_use ->
IO.puts("Claude is using: #{tool_use.name}")
end)
# Query with additional directory access and custom tools
session
|> ClaudeCode.query("Analyze the log files and create a summary report",
add_dir: ["/var/log", "/tmp/analysis"],
allowed_tools: ["View", "Edit"],
timeout: 180_000
)
|> ClaudeCode.Stream.text_content()
|> Enum.each(&IO.write/1)
# Handle all message types
session
|> ClaudeCode.query("Help me debug this code")
|> Enum.each(fn
%ClaudeCode.Message.Assistant{message: %{content: content}} ->
# Process assistant response
%ClaudeCode.Message.Result{result: result} ->
# Final result
_ ->
# Other message types
:ok
end)
Query-Level Overrides
Override session defaults for specific queries:
# Override options per query
{:ok, response} = ClaudeCode.query_sync(session, "Complex task",
system_prompt: "Focus on performance optimization",
timeout: 300_000,
allowed_tools: ["Bash(git:*)"]
)
Session Continuity
ClaudeCode automatically maintains conversation context across queries within a session, just like the interactive Claude CLI. No configuration required!
Automatic Conversation Continuity
{:ok, session} = ClaudeCode.start_link(api_key: "sk-ant-...")
# First query establishes the conversation
{:ok, response1} = ClaudeCode.query_sync(session, "Hello, my name is Alice")
# => "Hello Alice! Nice to meet you."
# Subsequent queries remember the conversation context
{:ok, response2} = ClaudeCode.query_sync(session, "What's my name?")
# => "Your name is Alice, as you mentioned when we first met."
# Context is maintained across streaming queries too
session
|> ClaudeCode.query("Tell me more about yourself")
|> ClaudeCode.Stream.text_content()
|> Enum.each(&IO.write/1)
# Claude remembers the previous conversation
Session Management API
When you need explicit control over conversation context:
# Check current session ID (for debugging/logging)
{:ok, session_id} = ClaudeCode.get_session_id(session)
# => {:ok, "abc123-def456-session-id"}
# For a new session with no queries yet
{:ok, nil} = ClaudeCode.get_session_id(session)
# First query establishes the conversation
{:ok, response1} = ClaudeCode.query_sync(session, "Hello, my name is Alice")
# => "Hello Alice! Nice to meet you."
# Subsequent queries remember the conversation context
{:ok, response2} = ClaudeCode.query_sync(session, "What's my name?")
# => "Your name is Alice, as you mentioned when we first met."
# Clear session to start a fresh conversation
:ok = ClaudeCode.clear(session)
# Next query starts with no previous context
{:ok, response} = ClaudeCode.query_sync(session, "What's my name?")
# => "I don't have any information about your name..."
How It Works
- Automatic: Session IDs are captured from Claude CLI responses and automatically used for subsequent queries
- Transparent: Uses the CLI's
--resume
flag internally - no API changes needed - Persistent: Sessions persist for the lifetime of the GenServer process
- Stateful by Default: Conversations continue naturally, matching interactive CLI behavior
This provides the same conversational experience as using Claude Code interactively, but programmatically within your Elixir applications.
Options Reference
For complete documentation of all available options, see the ClaudeCode.Options
module:
# View session options schema
ClaudeCode.Options.session_schema()
# View query options schema
ClaudeCode.Options.query_schema()
Key points:
:api_key
is required for all sessions- Query options can override session defaults
- Some options (
:timeout
,:name
) are Elixir-specific - Most options map directly to Claude CLI flags
Run mix docs
and navigate to ClaudeCode.Options
for detailed option documentation including types, defaults, and validation rules.
API Reference
ClaudeCode Module
# Start a session
ClaudeCode.start_link(opts)
# See ClaudeCode.Options.session_schema() for all available options
# Synchronous query (blocks until complete)
ClaudeCode.query_sync(session, prompt, opts \\ [])
# Returns: {:ok, String.t()} | {:error, term()}
# Streaming query (returns Elixir Stream)
ClaudeCode.query(session, prompt, opts \\ [])
# Returns: Stream.t()
# Async query (sends messages to calling process)
ClaudeCode.query_async(session, prompt, opts \\ [])
# Returns: {:ok, reference()} | {:error, term()}
# Session management
ClaudeCode.alive?(session) # Check if session is running
ClaudeCode.stop(session) # Stop the session
ClaudeCode.get_session_id(session) # Get current session ID for conversation continuity
ClaudeCode.clear(session) # Clear session to start fresh conversation
ClaudeCode.Stream Module
# Extract text content from responses
ClaudeCode.Stream.text_content(stream)
# Extract tool usage blocks
ClaudeCode.Stream.tool_uses(stream)
# Filter messages by type
ClaudeCode.Stream.filter_type(stream, :assistant)
# Buffer text until sentence boundaries
ClaudeCode.Stream.buffered_text(stream)
Error Handling
case ClaudeCode.query_sync(session, "Hello") do
{:ok, response} ->
IO.puts(response)
{:error, :timeout} ->
IO.puts("Request timed out")
{:error, {:cli_not_found, msg}} ->
IO.puts("CLI error: #{msg}")
{:error, {:claude_error, msg}} ->
IO.puts("Claude error: #{msg}")
end
Documentation
- 🚀 Getting Started - Step-by-step tutorial for new users
- 💻 Examples - Real-world usage patterns and code samples
- 🔧 Troubleshooting - Common issues and solutions
Production Usage
Performance & Concurrency
ClaudeCode is designed for production use with multiple concurrent sessions:
# Multiple sessions for parallel processing
sessions = 1..4 |> Enum.map(fn _i ->
{:ok, session} = ClaudeCode.start_link(api_key: "...")
session
end)
# Process tasks in parallel
results = Task.async_stream(tasks, fn task ->
session = Enum.random(sessions) # Simple load balancing
ClaudeCode.query_sync(session, task.prompt)
end, max_concurrency: 4)
# Clean up
Enum.each(sessions, &ClaudeCode.stop/1)
Phoenix Integration
Add ClaudeCode to your supervision tree for web applications:
# lib/my_app/application.ex
def start(_type, _args) do
children = [
MyAppWeb.Endpoint,
{ClaudeCode, [
api_key: System.get_env("ANTHROPIC_API_KEY"),
name: :claude_session
]}
]
Supervisor.start_link(children, strategy: :one_for_one)
end
# Use in controllers/live views
ClaudeCode.query_sync(:claude_session, prompt)
Best Practices
Session Management:
# Use named sessions for long-running processes {:ok, _} = ClaudeCode.start_link(api_key: "...", name: :main_claude) # Use temporary sessions for isolated tasks {:ok, temp} = ClaudeCode.start_link(api_key: "...") result = ClaudeCode.query_sync(temp, prompt) ClaudeCode.stop(temp)
Error Handling:
defp safe_claude_query(session, prompt) do case ClaudeCode.query_sync(session, prompt, timeout: 30_000) do {:ok, response} -> {:ok, response} {:error, :timeout} -> {:error, "Request timed out"} {:error, reason} -> {:error, "Claude error: #{inspect(reason)}"} end end
Resource Management:
# Always clean up sessions try do {:ok, session} = ClaudeCode.start_link(api_key: "...") # ... use session after ClaudeCode.stop(session) end
Development
# Clone and install dependencies
git clone https://github.com/guess/claude_code.git
cd claude_code
mix deps.get
# Run tests
mix test
# Run quality checks (format, credo, dialyzer)
mix quality
Contributing
We welcome contributions! Please:
- Pick an unimplemented feature or bug fix
- Open an issue to discuss your approach
- Submit a PR with tests and documentation
License
MIT License
Architecture
The SDK uses a GenServer-based architecture where each Claude session is a separate process that spawns the Claude CLI as a subprocess. Communication happens via JSON streaming over stdout, with the CLI process exiting after each query (stateless).
┌─────────────┐ ┌─────────────────┐ ┌──────────────┐
│ Your Code │────▶│ ClaudeCode API │────▶│ Session │
└─────────────┘ └─────────────────┘ │ (GenServer) │
└──────┬───────┘
│
┌──────▼───────┐
│ CLI Process │
│ (Port) │
└──────────────┘
Built on top of the Claude Code CLI and designed for the Elixir community.