Packages
claude_code
0.13.3
0.36.5
0.36.4
0.36.3
0.36.2
0.36.1
0.36.0
0.35.0
0.34.0
0.33.1
0.32.2
0.32.0
0.31.0
0.30.0
0.29.0
0.28.0
0.27.0
0.26.0
0.25.0
0.24.0
0.23.0
0.22.0
0.21.0
0.20.0
0.19.0
0.18.0
0.17.0
0.16.0
0.15.0
0.14.0
0.13.3
0.13.2
0.13.1
0.13.0
0.12.0
0.11.0
0.10.0
0.9.0
0.8.1
0.8.0
0.7.0
0.6.0
0.5.0
0.4.0
0.3.0
0.2.0
0.1.0
Claude Agent SDK for Elixir – Build AI agents with Claude Code
Current section
Files
Jump to
Current section
Files
lib/claude_code/options.ex
defmodule ClaudeCode.Options do
@moduledoc """
Handles option validation and CLI flag conversion.
This module is the **single source of truth** for all ClaudeCode options.
It provides validation for session and query options using NimbleOptions,
converts Elixir options to CLI flags, and manages option precedence:
query > session > app config > environment variables > defaults.
## Session Options
Session options are used when starting a ClaudeCode session. Most options
can be overridden at the query level.
### API Key
- `:api_key` - Anthropic API key (string, optional - falls back to ANTHROPIC_API_KEY env var)
### Claude Configuration
- `:model` - Claude model to use (string, optional - CLI uses its default)
- `:fallback_model` - Fallback model if primary fails (string, optional)
- `:system_prompt` - Override system prompt (string, optional)
- `:append_system_prompt` - Append to system prompt (string, optional)
- `:max_turns` - Limit agentic turns in non-interactive mode (integer, optional)
- `:max_budget_usd` - Maximum dollar amount to spend on API calls (number, optional)
- `:agent` - Agent name for the session (string, optional)
Overrides the 'agent' setting. Different from `:agents` which defines custom agents.
- `:betas` - Beta headers to include in API requests (list of strings, optional)
Example: `["feature-x", "feature-y"]`
### Tool Control
- `:tools` - Specify the list of available tools from the built-in set (optional)
Use `:default` for all tools, `[]` to disable all, or a list of tool names.
Example: `tools: :default` or `tools: ["Bash", "Edit", "Read"]`
- `:allowed_tools` - List of allowed tools (list of strings, optional)
Example: `["View", "Bash(git:*)"]`
- `:disallowed_tools` - List of denied tools (list of strings, optional)
- `:add_dir` - Additional directories for tool access (list of strings, optional)
Example: `["/tmp", "/var/log"]`
### Advanced Options
- `:agents` - Custom agent definitions (map, optional)
Map of agent name to agent configuration. Each agent must have `description` and `prompt`.
Example: `%{"code-reviewer" => %{"description" => "Reviews code", "prompt" => "You are a code reviewer", "tools" => ["Read", "Edit"], "model" => "sonnet"}}`
- `:mcp_config` - Path to MCP servers JSON config file (string, optional)
- `:mcp_servers` - MCP server configurations as a map (map, optional)
Values can be a Hermes MCP module (atom), a module map with custom env, or a command config map.
Example: `%{"my-tools" => MyApp.MCPServer, "custom" => %{module: MyApp.MCPServer, env: %{"DEBUG" => "1"}}, "playwright" => %{command: "npx", args: ["@playwright/mcp@latest"]}}`
- `:strict_mcp_config` - Only use MCP servers from mcp_config/mcp_servers (boolean, default: false)
When true, ignores all global MCP configurations and only uses explicitly provided MCP config.
- `:permission_prompt_tool` - MCP tool for handling permission prompts (string, optional)
- `:permission_mode` - Permission handling mode (atom, default: :default)
Options: `:default`, `:accept_edits`, `:bypass_permissions`
- `:json_schema` - JSON Schema for structured output validation (string or map, optional)
When provided as a map, it will be JSON encoded automatically.
Example: `%{"type" => "object", "properties" => %{"name" => %{"type" => "string"}}, "required" => ["name"]}`
- `:settings` - Settings configuration (string or map, optional)
Can be a file path, JSON string, or map that will be JSON encoded
Example: `%{"feature" => true}` or `"/path/to/settings.json"`
- `:setting_sources` - List of setting sources to load (list of strings, optional)
Valid sources: `"user"`, `"project"`, `"local"`
Example: `["user", "project", "local"]`
### Elixir-Specific Options
- `:name` - GenServer process name (atom, optional)
- `:timeout` - Query timeout in milliseconds (timeout, default: 300_000) - **Elixir only, not passed to CLI**
- `:resume` - Session ID to resume a previous conversation (string, optional)
- `:fork_session` - When resuming, create a new session ID instead of reusing the original (boolean, optional)
Must be used with `:resume`. Creates a fork of the conversation.
- `:tool_callback` - Post-execution callback for tool monitoring (function, optional)
Receives a map with `:name`, `:input`, `:result`, `:is_error`, `:tool_use_id`, `:timestamp`
- `:cwd` - Current working directory (string, optional)
## Query Options
Query options can override session defaults for individual queries.
All session options except `:api_key` and `:name` can be used as query options.
## Option Precedence
Options are resolved in this order (highest to lowest priority):
1. Query-level options
2. Session-level options
3. Application configuration
4. Environment variables (ANTHROPIC_API_KEY for api_key)
5. Schema defaults
## Usage Examples
# Session with comprehensive options
{:ok, session} = ClaudeCode.start_link(
api_key: "sk-ant-...",
model: "opus",
fallback_model: "sonnet",
system_prompt: "You are an Elixir expert",
allowed_tools: ["View", "Edit", "Bash(git:*)"],
add_dir: ["/tmp", "/var/log"],
max_turns: 20,
timeout: 180_000,
permission_mode: :default
)
# Query with option overrides
ClaudeCode.query(session, "Help with testing",
system_prompt: "Focus on ExUnit patterns",
allowed_tools: ["View"],
timeout: 60_000
)
# Application configuration
# config/config.exs
config :claude_code,
model: "sonnet",
timeout: 120_000,
allowed_tools: ["View", "Edit"]
# Session with MCP servers configured inline
{:ok, session} = ClaudeCode.start_link(
mcp_servers: %{
# Hermes MCP server module - auto-generates stdio config
"my-tools" => MyApp.MCPServer,
# Hermes module with custom environment variables
"my-tools-debug" => %{module: MyApp.MCPServer, env: %{"DEBUG" => "1"}},
# Explicit command config for external MCP servers
"playwright" => %{command: "npx", args: ["@playwright/mcp@latest"]}
}
)
## Security Considerations
- **`:permission_mode`**: Controls permission handling behavior.
Use `:bypass_permissions` only in development environments.
- **`:add_dir`**: Grants tool access to additional directories.
Only include safe directories.
- **`:allowed_tools`**: Use tool restrictions to limit Claude's capabilities.
Example: `["View", "Bash(git:*)"]` allows read-only operations and git commands.
"""
@session_opts_schema [
# Elixir-specific options
api_key: [type: :string, doc: "Anthropic API key"],
name: [type: :atom, doc: "Process name for the session"],
timeout: [type: :timeout, default: 300_000, doc: "Query timeout in ms"],
resume: [type: :string, doc: "Session ID to resume a previous conversation"],
fork_session: [
type: :boolean,
default: false,
doc: "When resuming, create a new session ID instead of reusing the original"
],
adapter: [
type: {:tuple, [:atom, :any]},
doc: """
Optional adapter for testing. A tuple of `{module, name}` where:
- `module` implements the `ClaudeCode.Adapter` behaviour
- `name` is passed to the adapter's `stream/3` callback
Example:
adapter: {ClaudeCode.Test, MyApp.Chat}
"""
],
tool_callback: [
type: {:fun, 1},
doc: """
Optional callback invoked after each tool execution.
Receives a map with:
- `:name` - Tool name (string)
- `:input` - Tool input (map)
- `:result` - Tool result (string)
- `:is_error` - Whether the tool errored (boolean)
- `:tool_use_id` - Unique ID for correlation (string)
- `:timestamp` - When the result was received (DateTime)
The callback is invoked asynchronously and should not block.
Example:
tool_callback: fn event ->
Logger.info("Tool \#{event.name} executed")
end
"""
],
# CLI options (aligned with TypeScript SDK)
model: [type: :string, doc: "Model to use"],
fallback_model: [type: :string, doc: "Fallback model to use if primary model fails"],
cwd: [type: :string, doc: "Current working directory"],
system_prompt: [type: :string, doc: "Override system prompt"],
append_system_prompt: [type: :string, doc: "Append to system prompt"],
max_turns: [type: :integer, doc: "Limit agentic turns in non-interactive mode"],
max_budget_usd: [type: {:or, [:float, :integer]}, doc: "Maximum dollar amount to spend on API calls"],
agent: [type: :string, doc: "Agent name for the session (overrides 'agent' setting)"],
betas: [type: {:list, :string}, doc: "Beta headers to include in API requests"],
tools: [
type: {:or, [{:in, [:default]}, {:list, :string}]},
doc: "Available tools: :default for all, [] for none, or list of tool names"
],
allowed_tools: [type: {:list, :string}, doc: ~s{List of allowed tools (e.g. ["View", "Bash(git:*)"])}],
disallowed_tools: [type: {:list, :string}, doc: "List of denied tools"],
agents: [
type: {:map, :string, {:map, :string, :any}},
doc:
"Custom agent definitions. Map of agent name to config with 'description', 'prompt', 'tools' (optional), 'model' (optional)"
],
mcp_config: [type: :string, doc: "Path to MCP servers JSON config file"],
mcp_servers: [
type: {:map, :string, {:or, [:atom, :map]}},
doc:
~s(MCP server configurations. Values can be a Hermes module atom or a config map. Example: %{"my-tools" => MyApp.MCPServer, "playwright" => %{command: "npx", args: ["@playwright/mcp@latest"]}})
],
strict_mcp_config: [
type: :boolean,
default: false,
doc: "Only use MCP servers from mcp_config/mcp_servers, ignoring global MCP configurations"
],
permission_prompt_tool: [type: :string, doc: "MCP tool for handling permission prompts"],
permission_mode: [
type: {:in, [:default, :accept_edits, :bypass_permissions]},
default: :default,
doc: "Permission handling mode"
],
add_dir: [type: {:list, :string}, doc: "Additional directories for tool access"],
json_schema: [
type: {:or, [:string, {:map, :string, :any}]},
doc: "JSON Schema for structured output validation (JSON string or map)"
],
settings: [
type: {:or, [:string, {:map, :string, :any}]},
doc: "Settings as file path, JSON string, or map to be JSON encoded"
],
setting_sources: [
type: {:list, :string},
doc: "List of setting sources to load (user, project, local)"
],
include_partial_messages: [
type: :boolean,
default: false,
doc: "Include partial message chunks as they arrive for character-level streaming"
]
]
@query_opts_schema [
# Query-level overrides for CLI options
model: [type: :string, doc: "Override model for this query"],
fallback_model: [type: :string, doc: "Override fallback model for this query"],
system_prompt: [type: :string, doc: "Override system prompt for this query"],
append_system_prompt: [type: :string, doc: "Append to system prompt for this query"],
max_turns: [type: :integer, doc: "Override max turns for this query"],
max_budget_usd: [type: {:or, [:float, :integer]}, doc: "Override max budget for this query"],
agent: [type: :string, doc: "Override agent for this query"],
betas: [type: {:list, :string}, doc: "Override beta headers for this query"],
tools: [
type: {:or, [{:in, [:default]}, {:list, :string}]},
doc: "Override available tools: :default for all, [] for none, or list"
],
allowed_tools: [type: {:list, :string}, doc: "Override allowed tools for this query"],
disallowed_tools: [type: {:list, :string}, doc: "Override disallowed tools for this query"],
agents: [
type: {:map, :string, :map},
doc: "Override agent definitions for this query"
],
mcp_servers: [
type: {:map, :string, {:or, [:atom, :map]}},
doc: "Override MCP server configurations for this query"
],
strict_mcp_config: [
type: :boolean,
doc: "Only use MCP servers from mcp_config/mcp_servers, ignoring global MCP configurations"
],
cwd: [type: :string, doc: "Override working directory for this query"],
timeout: [type: :timeout, doc: "Override timeout for this query"],
permission_mode: [
type: {:in, [:default, :accept_edits, :bypass_permissions]},
doc: "Override permission mode for this query"
],
add_dir: [type: {:list, :string}, doc: "Override additional directories for this query"],
json_schema: [
type: {:or, [:string, {:map, :string, :any}]},
doc: "JSON Schema for structured output validation (JSON string or map)"
],
settings: [
type: {:or, [:string, {:map, :string, :any}]},
doc: "Override settings for this query (file path, JSON string, or map)"
],
setting_sources: [
type: {:list, :string},
doc: "Override setting sources for this query (user, project, local)"
],
include_partial_messages: [
type: :boolean,
doc: "Include partial message chunks as they arrive for character-level streaming"
]
]
# App config uses same option names directly - no mapping needed
@doc """
Returns the session options schema.
"""
def session_schema, do: @session_opts_schema
@doc """
Returns the query options schema.
"""
def query_schema, do: @query_opts_schema
@doc """
Validates session options using NimbleOptions.
The CLI will handle API key resolution from the environment if not provided.
## Examples
iex> ClaudeCode.Options.validate_session_options([api_key: "sk-test"])
{:ok, [api_key: "sk-test", timeout: 300_000]}
iex> ClaudeCode.Options.validate_session_options([])
{:ok, [timeout: 300_000]}
"""
def validate_session_options(opts) do
validated = NimbleOptions.validate!(opts, @session_opts_schema)
{:ok, validated}
rescue
e in NimbleOptions.ValidationError ->
{:error, e}
end
@doc """
Validates query options using NimbleOptions.
## Examples
iex> ClaudeCode.Options.validate_query_options([timeout: 60_000])
{:ok, [timeout: 60_000]}
iex> ClaudeCode.Options.validate_query_options([invalid: "option"])
{:error, %NimbleOptions.ValidationError{}}
"""
def validate_query_options(opts) do
validated = NimbleOptions.validate!(opts, @query_opts_schema)
{:ok, validated}
rescue
e in NimbleOptions.ValidationError ->
{:error, e}
end
@doc """
Converts Elixir options to CLI arguments.
Ignores internal options like :api_key, :name, and :timeout that are not CLI flags.
## Examples
iex> ClaudeCode.Options.to_cli_args([system_prompt: "You are helpful"])
["--system-prompt", "You are helpful"]
iex> ClaudeCode.Options.to_cli_args([allowed_tools: ["View", "Bash(git:*)"]])
["--allowedTools", "View,Bash(git:*)"]
"""
def to_cli_args(opts) do
opts
|> Enum.reduce([], fn {key, value}, acc ->
case convert_option_to_cli_flag(key, value) do
{flag, flag_value} -> [flag_value, flag | acc]
# Handle multiple flag entries (like add_dir)
flag_entries when is_list(flag_entries) -> flag_entries ++ acc
nil -> acc
end
end)
|> Enum.reverse()
end
@doc """
Merges session and query options with query taking precedence.
## Examples
iex> session_opts = [timeout: 60_000, model: "sonnet"]
iex> query_opts = [timeout: 120_000]
iex> ClaudeCode.Options.merge_options(session_opts, query_opts)
[model: "sonnet", timeout: 120_000]
"""
def merge_options(session_opts, query_opts) do
Keyword.merge(session_opts, query_opts)
end
@doc """
Gets application configuration for claude_code.
Returns only valid option keys from the session schema.
"""
def get_app_config do
valid_keys = @session_opts_schema |> Keyword.keys() |> MapSet.new()
:claude_code
|> Application.get_all_env()
|> Enum.filter(fn {key, _value} -> MapSet.member?(valid_keys, key) end)
end
@doc """
Applies application config defaults to session options.
Session options take precedence over app config.
"""
def apply_app_config_defaults(session_opts) do
app_config = get_app_config()
# Apply app config first, then session opts
Keyword.merge(app_config, session_opts)
end
# Private functions
defp convert_option_to_cli_flag(:api_key, _value), do: nil
defp convert_option_to_cli_flag(:name, _value), do: nil
defp convert_option_to_cli_flag(:timeout, _value), do: nil
defp convert_option_to_cli_flag(:tool_callback, _value), do: nil
defp convert_option_to_cli_flag(:resume, _value), do: nil
defp convert_option_to_cli_flag(:adapter, _value), do: nil
defp convert_option_to_cli_flag(:fork_session, true) do
# Boolean flag without value - return as list to be flattened
["--fork-session"]
end
defp convert_option_to_cli_flag(:fork_session, false), do: nil
defp convert_option_to_cli_flag(_key, nil), do: nil
# TypeScript SDK aligned options
defp convert_option_to_cli_flag(:system_prompt, value) do
{"--system-prompt", to_string(value)}
end
defp convert_option_to_cli_flag(:append_system_prompt, value) do
{"--append-system-prompt", to_string(value)}
end
defp convert_option_to_cli_flag(:max_turns, value) do
{"--max-turns", to_string(value)}
end
defp convert_option_to_cli_flag(:max_budget_usd, value) do
{"--max-budget-usd", to_string(value)}
end
defp convert_option_to_cli_flag(:agent, value) do
{"--agent", to_string(value)}
end
defp convert_option_to_cli_flag(:betas, value) when is_list(value) do
if value == [] do
nil
else
Enum.flat_map(value, fn beta -> ["--betas", to_string(beta)] end)
end
end
defp convert_option_to_cli_flag(:tools, :default) do
{"--tools", "default"}
end
defp convert_option_to_cli_flag(:tools, []) do
# Empty list means no built-in tools - CLI accepts "" to disable all
{"--tools", ""}
end
defp convert_option_to_cli_flag(:tools, value) when is_list(value) do
tools_csv = Enum.join(value, ",")
{"--tools", tools_csv}
end
defp convert_option_to_cli_flag(:allowed_tools, value) when is_list(value) do
tools_csv = Enum.join(value, ",")
{"--allowedTools", tools_csv}
end
defp convert_option_to_cli_flag(:disallowed_tools, value) when is_list(value) do
tools_csv = Enum.join(value, ",")
{"--disallowedTools", tools_csv}
end
defp convert_option_to_cli_flag(:cwd, _value) do
# cwd is handled internally by changing working directory when spawning CLI process
# It's not passed as a CLI flag since the CLI doesn't support --cwd
nil
end
defp convert_option_to_cli_flag(:mcp_config, value) do
{"--mcp-config", to_string(value)}
end
defp convert_option_to_cli_flag(:permission_prompt_tool, value) do
{"--permission-prompt-tool", to_string(value)}
end
defp convert_option_to_cli_flag(:model, value) do
{"--model", to_string(value)}
end
defp convert_option_to_cli_flag(:fallback_model, value) do
{"--fallback-model", to_string(value)}
end
defp convert_option_to_cli_flag(:permission_mode, :default), do: nil
defp convert_option_to_cli_flag(:permission_mode, :accept_edits) do
{"--permission-mode", "acceptEdits"}
end
defp convert_option_to_cli_flag(:permission_mode, :bypass_permissions) do
{"--permission-mode", "bypassPermissions"}
end
defp convert_option_to_cli_flag(:add_dir, value) when is_list(value) do
if value == [] do
nil
else
# Return a flat list of alternating flags and values
Enum.flat_map(value, fn dir -> ["--add-dir", to_string(dir)] end)
end
end
defp convert_option_to_cli_flag(:json_schema, value) when is_map(value) do
json_string = Jason.encode!(value)
{"--json-schema", json_string}
end
defp convert_option_to_cli_flag(:json_schema, value) do
{"--json-schema", to_string(value)}
end
defp convert_option_to_cli_flag(:settings, value) when is_map(value) do
json_string = Jason.encode!(value)
{"--settings", json_string}
end
defp convert_option_to_cli_flag(:settings, value) do
{"--settings", to_string(value)}
end
defp convert_option_to_cli_flag(:setting_sources, value) when is_list(value) do
sources_csv = Enum.join(value, ",")
{"--setting-sources", sources_csv}
end
defp convert_option_to_cli_flag(:agents, value) when is_map(value) do
json_string = Jason.encode!(value)
{"--agents", json_string}
end
defp convert_option_to_cli_flag(:mcp_servers, value) when is_map(value) do
# Expand any module atoms to their stdio command config
expanded =
Map.new(value, fn
{name, module} when is_atom(module) ->
{name, expand_hermes_module(module, %{})}
{name, %{module: module} = config} when is_atom(module) ->
{name, expand_hermes_module(module, config)}
{name, %{"module" => module} = config} when is_atom(module) ->
{name, expand_hermes_module(module, config)}
{name, config} when is_map(config) ->
{name, config}
end)
# CLI expects mcpServers wrapper format via --mcp-config flag
json_string = Jason.encode!(%{mcpServers: expanded})
{"--mcp-config", json_string}
end
defp convert_option_to_cli_flag(:include_partial_messages, true) do
# Boolean flag without value - return as list to be flattened
["--include-partial-messages"]
end
defp convert_option_to_cli_flag(:include_partial_messages, false), do: nil
defp convert_option_to_cli_flag(:strict_mcp_config, true) do
# Boolean flag without value - return as list to be flattened
["--strict-mcp-config"]
end
defp convert_option_to_cli_flag(:strict_mcp_config, false), do: nil
defp convert_option_to_cli_flag(:input_format, :text) do
{"--input-format", "text"}
end
defp convert_option_to_cli_flag(:input_format, :stream_json) do
{"--input-format", "stream-json"}
end
# Internal options - not passed to CLI
defp convert_option_to_cli_flag(:callers, _value), do: nil
defp convert_option_to_cli_flag(:name, _value), do: nil
defp convert_option_to_cli_flag(:adapter, _value), do: nil
defp convert_option_to_cli_flag(:stub_name, _value), do: nil
defp convert_option_to_cli_flag(key, value) do
# Convert unknown keys to kebab-case flags
flag_name = "--" <> (key |> to_string() |> String.replace("_", "-"))
{flag_name, to_string(value)}
end
# Private helpers
defp expand_hermes_module(module, config) do
# Generate stdio command config for a Hermes MCP server module
# This allows the CLI to spawn the Elixir app with the MCP server
startup_code = "#{inspect(module)}.start_link(transport: :stdio)"
# Extract custom env from config (supports both atom and string keys)
custom_env = config[:env] || config["env"] || %{}
merged_env = Map.merge(%{"MIX_ENV" => "prod"}, custom_env)
%{
command: "mix",
args: ["run", "--no-halt", "-e", startup_code],
env: merged_env
}
end
end