This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
This is the ClaudeCode Elixir SDK - an idiomatic Elixir interface to the Claude Code CLI. The SDK spawns the claude command as a subprocess and communicates via streaming JSON over stdout.
mix quality # Run all code quality checks (compile, format, credo, dialyzer)
mix format # Format code with Styler
mix credo --strict # Run Credo analysis
mix dialyzer # Run Dialyzer type checkingmix test # Run all tests
mix test test/path/to_test.exs # Run specific test file
mix test.all # Run tests with coverage report
mix coveralls.html # Generate HTML coverage report
# Distributed tests (Adapter.Node) require a named BEAM node:
elixir --sname test -S mix test --include distributedmix deps.get # Install dependencies
mix claude_code.install # Install CLI binary to priv/bin/
mix claude_code.uninstall # Remove bundled CLI binary
mix claude_code.path # Print resolved CLI binary path
iex -S mix # Start interactive shell with project loaded
mix docs # Generate documentationThe SDK is organized in three layers:
- Adapter-agnostic — Session, Stream, Options (validation), Types, Message/Content structs
- CLI protocol — CLI.Command (flags), CLI.Input (stdin), CLI.Parser (JSON parsing)
- Adapters — Adapter.Port (local CLI via Port), Adapter.Node (distributed via BEAM), Adapter.Test (in-memory stubs)
Key modules:
- ClaudeCode.Session - Public API for session operations (lifecycle, runtime config, MCP, introspection, execute)
- ClaudeCode.Session.Server - GenServer that manages the adapter subprocess lifecycle
- ClaudeCode.Adapter.Port - Port-based adapter that spawns the CLI as a local subprocess
- ClaudeCode.Adapter.Node - Distributed adapter that runs Adapter.Port on a remote BEAM node via Erlang distribution
- ClaudeCode.CLI - Thin facade: binary resolution + command building (delegates to Resolver + Command)
- ClaudeCode.CLI.Command - Converts Elixir options to CLI flags and builds argument lists
- ClaudeCode.CLI.Parser - Parses newline-delimited JSON from CLI output into structs
- ClaudeCode.Adapter.Port.Installer - CLI binary download and version management
- ClaudeCode.Plugin - Plugin management (list, install, uninstall, enable, disable, update)
- ClaudeCode.Plugin.Marketplace - Marketplace management (list, add, remove, update)
Key CLI flags used:
--input-format stream-json- Bidirectional streaming mode (reads from stdin)--output-format stream-json- Get structured JSON output--verbose- Include all message types
26 features implemented (96% of core functionality) - See docs/proposals/FEATURE_MATRIX.md
Core capabilities:
- CLI installer with automatic binary management
- Session management with GenServer
- Synchronous and async query interface
- Streaming support with native Elixir Streams
- Message parsing (System, Assistant, User, Result, StreamEvent)
- Content blocks (Text, ToolUse, ToolResult, Thinking)
- Options API with NimbleOptions validation
- Model selection with fallback model support
- System prompts (override and append)
- Tool control (allowed/disallowed tools, additional directories)
- Permission modes and MCP integration
- Custom agents configuration
- Team settings loading (file path, JSON, or map)
- Session tracking and auto-resume
- Partial message streaming (character-level deltas)
- Stream utilities (text_deltas, thinking_deltas, buffered_text)
- Session forking (via
:fork_sessionoption with:resume)
- Unit tests mock the Port for predictable message sequences
- Integration tests use a mock CLI script when the real CLI isn't available
- Property-based testing with StreamData for message parsing
- All new code requires tests
- The SDK does NOT make direct API calls - all API communication is handled by the CLI
- Sessions use a persistent CLI subprocess with bidirectional streaming (stdin/stdout)
- The CLI auto-connects on first query and auto-disconnects on session stop
- API keys are passed via environment variables, never in command arguments
- Response content comes from the "result" message, not "assistant" messages
- Spawns the CLI binary directly via
spawn_executable(no shell wrapper) - Multi-turn conversations are supported via persistent connection (no subprocess restart between queries)
- Atom safety: use
ClaudeCode.MapUtils.safe_atomize_keysfor map keys from JSON payloads to prevent atom table exhaustion.String.to_atomis fine for enum values with finite sets (type, subtype, stop_reason, etc.) since the CLI is a trusted source with a bounded protocol.
lib/claude_code/- Main implementationsession.ex- Public API for session operations (delegates to Session.Server)session/server.ex- GenServer for session management with options validationpermission_mode.ex- Permission mode enum parsingpermission_denial.ex- Permission denial structaccount_info.ex- Account info from initialize responseagent_info.ex- Agent info from initialize responseslash_command.ex- Slash command from initialize response
options.ex- Options validation (NimbleOptions);to_cli_argsdelegates to CLI.Commandstream.ex- Stream utilities for real-time processingmessage.ex- Message type union + helpers + stop_reason parsing;parsedelegates to CLI.Parsercontent.ex- Content type union + helpers;parsedelegates to CLI.Parsermessage/- Message type modules (system, assistant, user, result, partial, tool_progress, etc.)message/system_message/- System message subtypes (init, status, hook_, task_, compact_boundary, etc.)content/- Content block modules (text, tool_use, tool_result, thinking, server_tool_use, mcp_tool_use, etc.)plugin.ex- Plugin management (list, install, uninstall, enable, disable, update, validate)plugin/marketplace.ex- Marketplace management (list, add, remove, update)
model.ex- Model namespace modulemodel/- Model-related typesinfo.ex- Model info struct (capabilities, effort levels)usage.ex- Per-model token usage struct and parsingeffort.ex- Effort level enum parsing
usage.ex- Shared usage parsing utilitiessystem.ex- System command abstraction (formerly system_cmd.ex)adapter.ex- Adapter behaviour definition + notification helpersadapter/port.ex- Port-based CLI adapter GenServer (Port, shell, env, reconnect, filter_system_env)port/installer.ex- CLI binary download and installationresolver.ex- CLI binary resolution and validation
node.ex- Distributed adapter (runs Adapter.Port on a remote BEAM node)test.ex- Test adapter (mock)
cli.ex- Thin facade: find_binary + build_command (delegates to Resolver + Command)cli/command.ex- CLI flag conversion and argument building (shared CLI protocol)input.ex- stream-json stdin message builders (shared CLI protocol)parser.ex- JSON → struct parsing (shared CLI protocol)
lib/mix/tasks/- Mix tasksclaude_code.install.ex- CLI installation mix taskclaude_code.uninstall.ex- CLI binary removalclaude_code.path.ex- CLI binary path resolution
test/- Test files mirror lib structuredocs/plans/- Design documents and implementation plansexamples/- Working examples
- Check
docs/proposals/FEATURE_MATRIX.mdfor prioritized features - Write tests first (TDD approach)
- Implement features
- Run
mix qualitybefore committing - Update documentation as needed
See ClaudeCode.Options module documentation for the authoritative source of all options, including:
- Complete schema definitions with NimbleOptions validation
- Type specifications and documentation
- Default values and precedence rules
- CLI flag mappings
Quick reference for development:
api_key(optional) - Anthropic API key (defaults to ANTHROPIC_API_KEY env var)- Options grouped by: Claude config, tool control, advanced features, Elixir-specific Key options:
:resume- Session ID to resume a previous conversation (passed tostart_link/1):continue- Continue the most recent conversation in the current directory (boolean):agents- List ofClaudeCode.Agentstructs or map of custom agent configurations:settings- Team settings (file path, JSON string, or map - auto-encoded to JSON):setting_sources- List of setting sources ([:user, :project, :local]):plugins- Plugin configurations (list of paths or maps with type: :local):allowed_tools/:disallowed_tools- Tool access control:system_prompt/:append_system_prompt- Custom system instructions:model- Claude model selection:fallback_model- Fallback model if primary fails:max_turns- Conversation turn limiting:max_thinking_tokens- Maximum tokens for thinking blocks (deprecated: use:thinking):thinking- Extended thinking config (:adaptive,{:enabled, budget_tokens: N}, or:disabled):effort- Effort level for the session (:low, :medium, :high):output_format- Structured output format (map with type: :json_schema and schema keys):mcp_config/:permission_prompt_tool- MCP integration:add_dir- Additional accessible directories:include_partial_messages- Enable character-level streaming:cli_path- CLI binary resolution mode::bundled(default),:global, or explicit path string:sandbox- Sandbox settings for bash command isolation (map, merged into --settings):inherit_env- Controls system env inheritance::all(default),[], or list of strings/{:prefix, "..."}tuples:env- Additional environment variables (map of string keys to string values orfalseto unset):enable_file_checkpointing- Enable file checkpointing (boolean, set via env var):can_use_tool- Permission prompt callback (maps to--permission-prompt-tool stdio):mcp_servers- Inline MCP server config (module shorthand or full config maps):file- List of file specs to attach to the session:from_pr- Resume a session linked to a PR number or URL:worktree- Git worktree support (boolean or branch name):agent- Agent selection for the session:tools- Built-in tool selection (:default, list, or[]to disable all):max_budget_usd- Maximum budget cap in USD:session_id- Explicit session ID:cwd- Working directory override (handled internally, not a CLI flag):betas- Beta feature flags (list of strings):debug/:debug_file- Debug mode and log file path:tool_config/:prompt_suggestions- Sent via control protocol initialize
Application config options:
:cli_version- Version to install (default: SDK's tested version):cli_path- CLI binary resolution mode::bundled(default),:global, or explicit path:cli_dir- Directory for downloaded binary (default: priv/bin/)
All message types follow the official Claude SDK schema:
# System messages (init)
%ClaudeCode.Message.SystemMessage{
type: :system,
subtype: :init,
session_id: "uuid",
tools: ["Bash", "Read", ...],
model: "claude-sonnet-...",
permission_mode: :default,
...
}
# Assistant messages (nested structure)
%ClaudeCode.Message.AssistantMessage{
message: %{
content: [%ClaudeCode.Content.TextBlock{text: "..."} | %ClaudeCode.Content.ToolUseBlock{...}],
context_management: %{...} # Optional context management info from API
}
}
# User messages (nested structure)
%ClaudeCode.Message.UserMessage{
message: %{
content: [%ClaudeCode.Content.TextBlock{text: "..."} | %ClaudeCode.Content.ToolResultBlock{...}]
}
}
# Result messages (final response)
%ClaudeCode.Message.ResultMessage{
result: "final response text",
is_error: false,
subtype: nil # or :error_max_turns, :error_during_execution
}# Text content
%ClaudeCode.Content.TextBlock{text: "response text"}
# Tool usage
%ClaudeCode.Content.ToolUseBlock{
id: "tool_id",
name: "tool_name",
input: %{...}
}
# Tool results
%ClaudeCode.Content.ToolResultBlock{
tool_use_id: "tool_id",
content: "tool output",
is_error: false
}
# Extended thinking
%ClaudeCode.Content.ThinkingBlock{
thinking: "reasoning content",
signature: "signature_value"
}# Extract only text content from assistant messages
ClaudeCode.Stream.text_content(stream)
# Extract only thinking content from assistant messages
ClaudeCode.Stream.thinking_content(stream)
# Extract only tool usage blocks
ClaudeCode.Stream.tool_uses(stream)
# Filter by message type
ClaudeCode.Stream.filter_type(stream, :assistant)
# Buffer text until sentence boundaries
ClaudeCode.Stream.buffered_text(stream)
# Take messages until result is received
ClaudeCode.Stream.until_result(stream)
# Character-level streaming (requires include_partial_messages: true)
ClaudeCode.Stream.text_deltas(stream) # Text character deltas
ClaudeCode.Stream.thinking_deltas(stream) # Thinking character deltas
ClaudeCode.Stream.content_deltas(stream) # All delta types with index# CLI errors
{:error, {:cli_not_found, message}}
{:error, {:cli_exit, exit_code}}
{:error, {:port_closed, reason}}
# Claude API errors (via Result message)
%ClaudeCode.Message.Result{
is_error: true,
subtype: :error_max_turns, # or :error_during_execution
result: "Error details..."
}
# Stream errors
{:stream_init_error, reason}
{:stream_error, error}
{:stream_timeout, request_ref}Options are validated using NimbleOptions and follow this precedence:
- Session-level options (highest priority)
- Application configuration
- Default values (lowest priority)
The ClaudeCode.Options module handles validation and conversion to CLI flags.
- Never add a "Co-Authored-By" line to commit messages or PR descriptions
- In CHANGELOG.md, always use fully qualified module names (e.g.,
ClaudeCode.Stream.filter_type/2, notStream.filter_type/2). ExDoc parses the changelog and will emit warnings for unresolved short names.
- When creating mock data for tests, run the real commands and print the outputs so that we can mimic the actual response and make sure we are handling them appropriately.
- CLI messages have evolved through phases - ensure tests cover all message types
- Assistant/User messages use nested structure with
message.content, not direct.content - Response content comes from "result" message, not "assistant" messages for final answers
- Option validation happens at session start and query time - use NimbleOptions for consistent validation