Packages
claude_code
0.3.0
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.
### Required Options
- `:api_key` - Anthropic API key (string, required - falls back to ANTHROPIC_API_KEY env var)
### Claude Configuration
- `:model` - Claude model to use (string, optional - CLI uses its default)
- `: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)
### Tool Control
- `: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
- `:mcp_config` - Path to MCP servers JSON config file (string, optional)
- `: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`
### Elixir-Specific Options
- `:name` - GenServer process name (atom, optional)
- `:timeout` - Query timeout in milliseconds (timeout, default: 300_000) - **Elixir only, not passed to CLI**
- `:permission_handler` - Custom permission handler module (atom, optional)
- `:cwd` - Current working directory (string, optional)
## Query Options
Query options can override session defaults for individual queries.
All session options except `:api_key`, `:name`, and `:permission_handler`
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",
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"]
## 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, required: true, doc: "Anthropic API key"],
name: [type: :atom, doc: "Process name for the session"],
timeout: [type: :timeout, default: 300_000, doc: "Query timeout in ms"],
permission_handler: [type: :atom, doc: "Custom permission handler module"],
# CLI options (aligned with TypeScript SDK)
model: [type: :string, doc: "Model to use"],
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"],
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"],
mcp_config: [type: :string, doc: "Path to MCP servers JSON config file"],
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"]
]
@query_opts_schema [
# Query-level overrides for CLI options
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"],
allowed_tools: [type: {:list, :string}, doc: "Override allowed tools for this query"],
disallowed_tools: [type: {:list, :string}, doc: "Override disallowed tools for this query"],
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"]
]
# 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.
If no `:api_key` is provided in options, falls back to checking the
`ANTHROPIC_API_KEY` environment variable.
## 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([])
{:error, %NimbleOptions.ValidationError{}}
"""
def validate_session_options(opts) do
# Check for API key fallback before validation
opts_with_api_key_fallback = maybe_add_api_key_from_env(opts)
validated = NimbleOptions.validate!(opts_with_api_key_fallback, @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, :timeout, and :permission_handler 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, which takes
precedence over environment variables.
"""
def apply_app_config_defaults(session_opts) do
app_config = get_app_config()
# Apply environment variable fallback first, then app config, then session opts
[]
|> maybe_add_api_key_from_env()
|> Keyword.merge(app_config)
|> Keyword.merge(session_opts)
end
@doc """
Resolves final options using complete precedence chain.
Precedence: query > session > app config > environment variables > defaults
"""
def resolve_final_options(session_opts, query_opts) do
# Extract defaults from schema
defaults = extract_defaults_from_schema(@session_opts_schema)
env_fallback = maybe_add_api_key_from_env([])
# Apply precedence chain: defaults -> env vars -> app config -> session -> query
defaults
|> Keyword.merge(env_fallback)
|> Keyword.merge(get_app_config())
|> Keyword.merge(session_opts)
|> Keyword.merge(query_opts)
end
# Private functions
defp maybe_add_api_key_from_env(opts) do
case Keyword.get(opts, :api_key) do
nil ->
# No api_key provided, check environment variable
case System.get_env("ANTHROPIC_API_KEY") do
nil -> opts
api_key -> Keyword.put(opts, :api_key, api_key)
end
_ ->
# api_key already provided, use as-is
opts
end
end
defp extract_defaults_from_schema(schema) do
Enum.reduce(schema, [], fn {key, opts}, acc ->
case Keyword.get(opts, :default) do
nil -> acc
default -> Keyword.put(acc, key, default)
end
end)
end
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(:permission_handler, _value), do: nil
defp convert_option_to_cli_flag(:timeout, _value), 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(: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(: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(key, value) do
# Convert unknown keys to kebab-case flags
flag_name = "--" <> (key |> to_string() |> String.replace("_", "-"))
{flag_name, to_string(value)}
end
end