Packages
claude_code
0.32.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 """
Option validation, CLI flag conversion, and configuration guide.
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.
## Option Precedence
Options are resolved in this order (highest to lowest priority):
1. **Query-level options** — passed to `query/3` or `stream/3`
2. **Session-level options** — passed to `start_link/1`
3. **Application config** — set in `config/config.exs`
4. **Default values** — built-in defaults
# Application config (lowest priority)
config :claude_code, timeout: 300_000
# Session-level overrides app config
{:ok, session} = ClaudeCode.start_link(timeout: 120_000)
# Query-level overrides session
ClaudeCode.stream(session, "Hello", timeout: 60_000)
## Session Options
All options for `ClaudeCode.start_link/1`:
### Authentication
| Option | Type | Default | Description |
| --------- | ------ | ----------------------- | ---------------------------- |
| `api_key` | string | `ANTHROPIC_API_KEY` env | Anthropic API key |
| `name` | atom | - | Register session with a name |
### Model Configuration
| Option | Type | Default | Description |
| ---------------------- | ------- | -------- | ------------------------------------------- |
| `model` | string | "sonnet" | Claude model to use |
| `fallback_model` | string | - | Fallback if primary model fails |
| `system_prompt` | string | - | Override system prompt |
| `append_system_prompt` | string | - | Append to default system prompt |
| `max_turns` | integer | - | Limit conversation turns |
| `max_budget_usd` | number | - | Maximum dollar amount to spend on API calls |
| `agent` | string | - | Agent name for the session |
| `betas` | list | - | Beta headers for API requests |
| `max_thinking_tokens` | integer | - | Deprecated: use `thinking` instead |
| `thinking` | atom/tuple | - | Thinking config: `:adaptive`, `:disabled`, `{:enabled, budget_tokens: N}` |
| `effort` | atom | - | Effort level: `:low`, `:medium`, `:high`, `:max` |
### Timeouts
| Option | Type | Default | Description |
| --------- | ------- | --------- | ------------------------------------------------------------- |
| `timeout` | timeout | :infinity | Max wait for next message on the stream (ms or :infinity) |
### Tool Control
| Option | Type | Default | Description |
| ------------------ | --------- | ---------- | --------------------------------------------------- |
| `tools` | atom/list | - | Available tools: `:default`, `[]`, or list of names |
| `allowed_tools` | list | - | Tools Claude can use |
| `disallowed_tools` | list | - | Tools Claude cannot use |
| `add_dir` | list | - | Additional accessible directories |
| `permission_mode` | atom | `:default` | Permission handling mode |
### Advanced
| Option | Type | Default | Description |
| -------------------------- | ---------- | ----------- | --------------------------------------------------------------- |
| `adapter` | tuple | CLI adapter | Backend adapter as `{Module, config}` tuple |
| `resume` | string | - | Session ID to resume |
| `fork_session` | boolean | false | Create new session ID when resuming |
| `continue` | boolean | false | Continue most recent conversation in current directory |
| `mcp_config` | string | - | Path to MCP config file |
| `strict_mcp_config` | boolean | false | Only use MCP servers from explicit config |
| `agents` | list/map | - | Custom agent configurations (list of `Agent` structs or map) |
| `settings` | map/string | - | Team settings |
| `setting_sources` | list | - | Setting source priority |
| `include_partial_messages` | boolean | false | Enable character-level streaming |
| `output_format` | map | - | Structured output format (see Structured Outputs section) |
| `plugins` | list | - | Plugin configurations to load (paths or maps with type: :local) |
## Query Options
Options that can be passed to `stream/3`:
| Option | Type | Description |
| -------------------------- | ------- | --------------------------------------- |
| `timeout` | timeout | Override stream timeout |
| `system_prompt` | string | Override system prompt for this query |
| `append_system_prompt` | string | Append to system prompt |
| `max_turns` | integer | Limit turns for this query |
| `max_budget_usd` | number | Maximum dollar amount for this query |
| `agent` | string | Agent to use for this query |
| `betas` | list | Beta headers for this query |
| `max_thinking_tokens` | integer | Deprecated: use `thinking` instead |
| `thinking` | atom/tuple | Override thinking config for this query |
| `effort` | atom | Override effort level for this query |
| `tools` | list | Available tools for this query |
| `allowed_tools` | list | Allowed tools for this query |
| `disallowed_tools` | list | Disallowed tools for this query |
| `output_format` | map | Structured output format for this query |
| `plugins` | list | Plugin configurations for this query |
| `include_partial_messages` | boolean | Enable deltas for this query |
Note: `api_key` and `name` cannot be overridden at query time.
## Application Configuration
Set defaults in `config/config.exs`:
config :claude_code,
api_key: System.get_env("ANTHROPIC_API_KEY"),
model: "sonnet",
timeout: 180_000,
system_prompt: "You are a helpful assistant",
allowed_tools: ["View"]
### CLI Configuration
The SDK manages the Claude CLI binary via the `:cli_path` option:
config :claude_code,
cli_path: :bundled, # :bundled (default), :global, or "/path/to/claude"
cli_version: "x.y.z", # Version to install (default: SDK's tested version)
cli_dir: nil # Directory for downloaded binary (default: priv/bin/)
Resolution modes:
| Mode | Value | Behavior |
| -------- | ---------------------- | ------------------------------------------------------------------------------- |
| Bundled | `:bundled` (default) | Uses priv/bin/ binary. Auto-installs if missing. Verifies version matches SDK. |
| Global | `:global` | Finds existing system install via PATH or common locations. No auto-install. |
| Explicit | `"/path/to/claude"` | Uses that exact binary. Error if not found. |
Mix tasks:
mix claude_code.install # Install or update to SDK's tested version
mix claude_code.install --version x.y.z # Install specific version
mix claude_code.install --force # Force reinstall even if version matches
mix claude_code.uninstall # Remove the bundled CLI binary
mix claude_code.path # Print the resolved CLI binary path
For releases:
# Option 1: Pre-install during release build (recommended)
# (Run mix claude_code.install before building the release)
# Option 2: Configure writable directory for runtime download
config :claude_code, cli_dir: "/var/lib/claude_code"
# Option 3: Use system-installed CLI
config :claude_code, cli_path: :global
### Environment-Specific Configuration
# config/dev.exs
config :claude_code,
timeout: 60_000,
permission_mode: :accept_edits
# config/prod.exs
config :claude_code,
timeout: :infinity,
permission_mode: :default
# config/test.exs
config :claude_code,
api_key: "test-key",
timeout: 5_000
## Model Selection
# Use a specific model
{:ok, session} = ClaudeCode.start_link(model: "opus")
# With fallback
{:ok, session} = ClaudeCode.start_link(
model: "opus",
fallback_model: "sonnet"
)
Available models: `"sonnet"`, `"opus"`, `"haiku"`, or full model IDs.
## System Prompts
# Override completely
{:ok, session} = ClaudeCode.start_link(
system_prompt: "You are an Elixir expert. Only discuss Elixir."
)
# Append to default
{:ok, session} = ClaudeCode.start_link(
append_system_prompt: "Always format code with proper indentation."
)
## Cost Control
# Limit spending per query
session
|> ClaudeCode.stream("Complex analysis task", max_budget_usd: 5.00)
|> Stream.run()
# Set a session-wide budget limit
{:ok, session} = ClaudeCode.start_link(
max_budget_usd: 25.00
)
## Structured Outputs
Use the `:output_format` option with a JSON Schema to get validated structured responses:
schema = %{
"type" => "object",
"properties" => %{
"name" => %{"type" => "string"},
"age" => %{"type" => "integer"},
"skills" => %{"type" => "array", "items" => %{"type" => "string"}}
},
"required" => ["name", "age"]
}
session
|> ClaudeCode.stream("Extract person info from: John is 30 and knows Elixir",
output_format: %{type: :json_schema, schema: schema})
|> ClaudeCode.Stream.text_content()
|> Enum.join()
The `:output_format` option accepts a map with:
- `:type` — currently only `:json_schema` is supported
- `:schema` — a JSON Schema map defining the expected structure
## Tool Configuration
# Use all default tools
{:ok, session} = ClaudeCode.start_link(tools: :default)
# Specify available tools (subset of built-in)
{:ok, session} = ClaudeCode.start_link(
tools: ["Bash", "Edit", "Read"]
)
# Disable all tools
{:ok, session} = ClaudeCode.start_link(tools: [])
# Allow specific tools with patterns
{:ok, session} = ClaudeCode.start_link(
allowed_tools: ["View", "Edit", "Bash(git:*)"]
)
# Disallow specific tools
{:ok, session} = ClaudeCode.start_link(
disallowed_tools: ["Bash", "Write"]
)
# Additional directories
{:ok, session} = ClaudeCode.start_link(
add_dir: ["/app/lib", "/app/test"]
)
## MCP Server Control
Claude Code can connect to MCP (Model Context Protocol) servers for additional
tools. By default, it uses globally configured MCP servers. Use `strict_mcp_config`
to control this:
# No tools at all (no built-in tools, no MCP servers)
{:ok, session} = ClaudeCode.start_link(
tools: [],
strict_mcp_config: true
)
# Built-in tools only (ignore global MCP servers)
{:ok, session} = ClaudeCode.start_link(
tools: :default,
strict_mcp_config: true
)
# Default behavior (built-in tools + global MCP servers)
{:ok, session} = ClaudeCode.start_link()
# Specific MCP servers only (no global config)
{:ok, session} = ClaudeCode.start_link(
strict_mcp_config: true,
mcp_servers: %{
"my-tools" => %{command: "npx", args: ["my-mcp-server"]}
}
)
### Using Hermes MCP Modules
You can use Elixir-based MCP servers built with Hermes MCP:
{:ok, session} = ClaudeCode.start_link(
strict_mcp_config: true,
mcp_servers: %{
"my-tools" => MyApp.MCPServer,
"custom" => %{module: MyApp.MCPServer, env: %{"DEBUG" => "1"}}
}
)
## Custom Agents
Configure custom agents with `ClaudeCode.Agent` structs:
alias ClaudeCode.Agent
agents = [
Agent.new(
name: "code-reviewer",
description: "Expert code reviewer",
prompt: "You review code for quality and best practices.",
tools: ["View", "Grep", "Glob"],
model: "sonnet"
)
]
{:ok, session} = ClaudeCode.start_link(agents: agents)
Raw maps are also accepted for backwards compatibility:
{:ok, session} = ClaudeCode.start_link(agents: %{
"code-reviewer" => %{
"description" => "Expert code reviewer",
"prompt" => "You review code for quality and best practices."
}
})
See the Subagents Guide for more details.
## Team Settings
# From file path
{:ok, session} = ClaudeCode.start_link(
settings: "/path/to/settings.json"
)
# From map (auto-encoded to JSON)
{:ok, session} = ClaudeCode.start_link(
settings: %{
"team_name" => "My Team",
"preferences" => %{"theme" => "dark"}
}
)
# Control setting sources
{:ok, session} = ClaudeCode.start_link(
setting_sources: ["user", "project", "local"]
)
## Plugins
Load custom plugins to extend Claude's capabilities:
# From a directory path
{:ok, session} = ClaudeCode.start_link(
plugins: ["./my-plugin"]
)
# With explicit type (currently only :local is supported)
{:ok, session} = ClaudeCode.start_link(
plugins: [
%{type: :local, path: "./my-plugin"},
"./another-plugin"
]
)
For marketplace plugin management (install, enable, disable), see
`ClaudeCode.Plugin` and `ClaudeCode.Plugin.Marketplace`.
## Runtime Control
Some settings can be changed mid-conversation without restarting the session,
using the bidirectional control protocol:
# Switch model on the fly
:ok = ClaudeCode.Session.set_model(session, "opus")
# Change permission mode
:ok = ClaudeCode.Session.set_permission_mode(session, :bypass_permissions)
# Query MCP server status
{:ok, status} = ClaudeCode.Session.mcp_status(session)
See the Sessions guide for more details.
## Validation Errors
Invalid options raise descriptive errors:
{:ok, session} = ClaudeCode.start_link(timeout: "not a number")
# => ** (NimbleOptions.ValidationError) invalid value for :timeout option:
# expected positive integer, got: "not a number"
## 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.
"""
require Logger
@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: :infinity,
doc:
"Max time in ms to wait for the next message on the stream. Resets on each message. Accepts a positive integer or :infinity."
],
cli_path: [
type: {:or, [{:in, [:bundled, :global]}, :string]},
doc: """
CLI binary resolution mode.
- `:bundled` (default) — Use priv/bin/ binary, auto-install if missing, verify version matches SDK's pinned version
- `:global` — Find existing system install via PATH or common locations, no auto-install
- `"/path/to/claude"` — Use exact binary path
Can also be set via application config: `config :claude_code, cli_path: :global`
"""
],
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"
],
continue: [
type: :boolean,
default: false,
doc: "Continue the most recent conversation in the current directory"
],
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}
"""
],
hooks: [
type: :map,
doc: """
Lifecycle hook configurations.
A map of event names to lists of hook entries. Each entry can be:
- A **bare module** or **2-arity function** (shorthand — registered without a matcher)
- A **map** with `:matcher`, `:hooks`, and optional `:timeout`
Shorthand:
hooks: %{
PreToolUse: [MyApp.BashGuard],
PostToolUse: [fn input, _id -> Logger.info(inspect(input)); :ok end]
}
Full form (required for matchers, timeouts, or `:where`):
hooks: %{
PreToolUse: [%{matcher: "Bash", hooks: [MyApp.BashGuard]}],
PostToolUse: [%{hooks: [MyApp.AuditLogger]}]
}
Mixed:
hooks: %{
PreToolUse: [
MyApp.GlobalGuard,
%{matcher: "Bash", hooks: [MyApp.BashGuard], timeout: 30}
]
}
"""
],
env: [
type: {:map, :string, :string},
default: %{},
doc: """
Environment variables to merge with system environment when spawning CLI.
These variables override system environment variables but are overridden by
SDK-required variables (CLAUDE_CODE_ENTRYPOINT, CLAUDE_CODE_SDK_VERSION) and
the `:api_key` option (which sets ANTHROPIC_API_KEY).
Merge precedence (lowest to highest):
1. System environment variables
2. User `:env` option (these values)
3. SDK-required variables
4. `:api_key` option
Useful for:
- MCP tools that need specific env vars
- Providing PATH or other tool-specific configuration
- Testing with custom environment
Example:
env: %{
"MY_CUSTOM_VAR" => "value",
"PATH" => "/custom/bin:" <> System.get_env("PATH")
}
"""
],
# 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"],
max_thinking_tokens: [type: :integer, doc: "Maximum tokens for thinking blocks (deprecated: use :thinking instead)"],
thinking: [
type: {:custom, __MODULE__, :validate_thinking, []},
doc: """
Extended thinking configuration. Takes precedence over :max_thinking_tokens.
- `:adaptive` — Use adaptive thinking (defaults to 32,000 token budget)
- `:disabled` — Disable extended thinking
- `{:enabled, budget_tokens: N}` — Enable with specific token budget
Example:
thinking: :adaptive
thinking: {:enabled, budget_tokens: 16_000}
"""
],
effort: [
type: {:in, [:low, :medium, :high, :max]},
doc: "Effort level for the session (:low, :medium, :high, :max)"
],
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, :delegate, :dont_ask, :plan]},
default: :default,
doc: "Permission handling mode (:default, :accept_edits, :bypass_permissions, :delegate, :dont_ask, :plan)"
],
add_dir: [type: {:list, :string}, doc: "Additional directories for tool access"],
output_format: [
type: :map,
doc: "Output format for structured outputs - map with type: :json_schema and schema keys"
],
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)"
],
plugins: [
type: {:list, {:or, [:string, :map]}},
doc: "Plugin configurations - list of paths or maps with type: :local and path keys"
],
include_partial_messages: [
type: :boolean,
default: false,
doc: "Include partial message chunks as they arrive for character-level streaming"
],
replay_user_messages: [
type: :boolean,
default: false,
doc: "Re-emit user messages from stdin back on stdout for acknowledgment (only works with stream-json input/output)"
],
allow_dangerously_skip_permissions: [
type: :boolean,
default: false,
doc:
"Enable bypassing all permission checks as an option. Required when using permission_mode: :bypass_permissions. Recommended only for sandboxes with no internet access."
],
dangerously_skip_permissions: [
type: :boolean,
default: false,
doc:
"Bypass all permission checks. Recommended only for sandboxes with no internet access. Unlike :allow_dangerously_skip_permissions, this directly enables bypassing without requiring a separate permission mode."
],
disable_slash_commands: [
type: :boolean,
default: false,
doc: "Disable all skills/slash commands"
],
no_session_persistence: [
type: :boolean,
default: false,
doc: "Disable session persistence - sessions will not be saved to disk and cannot be resumed"
],
session_id: [
type: :string,
doc: "Use a specific session ID for the conversation (must be a valid UUID)"
],
file: [
type: {:list, :string},
doc:
~s{File resources to download at startup. Format: file_id:relative_path (e.g. ["file_abc:doc.txt", "file_def:img.png"])}
],
from_pr: [
type: {:or, [:string, :integer]},
doc: "Resume a session linked to a PR by PR number or URL"
],
debug: [
type: {:or, [:boolean, :string]},
doc: ~s{Enable debug mode with optional category filtering (e.g. true or "api,hooks" or "!1p,!file")}
],
debug_file: [
type: :string,
doc: "Write debug logs to a specific file path (implicitly enables debug mode)"
],
sandbox: [
type: {:custom, __MODULE__, :validate_sandbox, []},
doc: """
Sandbox settings for bash command isolation (merged into CLI `--settings`).
Accepts a `ClaudeCode.Sandbox` struct, keyword list, or map.
Maps and keyword lists are converted to a `ClaudeCode.Sandbox` struct.
See `ClaudeCode.Sandbox` for all available fields.
## Examples
sandbox: ClaudeCode.Sandbox.new(
enabled: true,
filesystem: [allow_write: ["/tmp/build"]],
network: [allowed_domains: ["github.com"]]
)
# Keyword shorthand (auto-converted to struct)
sandbox: [enabled: true, filesystem: [allow_write: ["/tmp"]]]
"""
],
can_use_tool: [
type: {:custom, ClaudeCode.Options, :validate_can_use_tool, []},
type_doc: "module implementing `ClaudeCode.Hook` | `(map(), String.t() | nil -> term())`",
doc:
"Permission prompt callback. Receives tool info map and tool_use_id, returns a permission decision. Mutually exclusive with :permission_prompt_tool."
],
enable_file_checkpointing: [
type: :boolean,
default: false,
doc: "Enable file checkpointing to track file changes during the session (set via env var, not CLI flag)"
],
worktree: [
type: {:or, [:boolean, :string]},
doc: "Create a new git worktree for this session (true for auto-named, or string for custom name)"
],
prompt_suggestions: [
type: :boolean,
default: false,
doc: "Enable prompt suggestions - emits predicted next user prompts after each turn"
],
resume_session_at: [
type: :string,
doc: "When resuming, only resume messages up to and including the message with this UUID (use with :resume)"
],
tool_config: [
type: {:map, :string, :map},
doc:
~s|Per-tool configuration for built-in tools. Map of tool name to config map (e.g. %{"askUserQuestion" => %{"previewFormat" => "html"}})|
],
extra_args: [
type: {:list, :string},
default: [],
doc:
~s{Additional CLI arguments passed directly to the claude binary. Each element is a separate argument (e.g. ["--flag", "value"]).}
],
max_buffer_size: [
type: :pos_integer,
default: 1_048_576,
doc:
"Maximum buffer size in bytes for incoming JSON data. Protects against unbounded memory growth. Default: 1MB (1_048_576 bytes)."
]
]
@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"],
max_thinking_tokens: [
type: :integer,
doc: "Override max thinking tokens for this query (deprecated: use :thinking instead)"
],
thinking: [
type: {:custom, __MODULE__, :validate_thinking, []},
doc: "Override thinking config for this query (see session option for details)"
],
effort: [
type: {:in, [:low, :medium, :high, :max]},
doc: "Override effort level 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, :string, :any}},
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 stream timeout for this query"],
permission_mode: [
type: {:in, [:default, :accept_edits, :bypass_permissions, :delegate, :dont_ask, :plan]},
doc: "Override permission mode for this query"
],
add_dir: [type: {:list, :string}, doc: "Override additional directories for this query"],
output_format: [
type: :map,
doc: "Override output format for this query"
],
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)"
],
plugins: [
type: {:list, {:or, [:string, :map]}},
doc: "Override plugin configurations for this query"
],
include_partial_messages: [
type: :boolean,
doc: "Include partial message chunks as they arrive for character-level streaming"
],
disable_slash_commands: [
type: :boolean,
doc: "Disable all skills/slash commands for this query"
],
no_session_persistence: [
type: :boolean,
doc: "Disable session persistence for this query"
],
extra_args: [
type: {:list, :string},
default: [],
doc: "Additional CLI arguments passed directly to the claude binary 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.
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: :infinity]}
iex> ClaudeCode.Options.validate_session_options([])
{:ok, [timeout: :infinity]}
"""
def validate_session_options(opts) do
validated =
opts |> normalize_agents() |> NimbleOptions.validate!(@session_opts_schema)
warn_deprecated_max_thinking_tokens(validated)
validate_mutual_exclusions(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 = opts |> normalize_agents() |> NimbleOptions.validate!(@query_opts_schema)
warn_deprecated_max_thinking_tokens(validated)
{:ok, validated}
rescue
e in NimbleOptions.ValidationError ->
{:error, e}
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
@doc false
def validate_thinking(:adaptive), do: {:ok, :adaptive}
def validate_thinking(:disabled), do: {:ok, :disabled}
def validate_thinking({:enabled, opts}) when is_list(opts) do
case Keyword.fetch(opts, :budget_tokens) do
{:ok, budget} when is_integer(budget) and budget > 0 ->
{:ok, {:enabled, opts}}
{:ok, _} ->
{:error, "expected :budget_tokens to be a positive integer"}
:error ->
{:error, "expected {:enabled, budget_tokens: pos_integer}, missing :budget_tokens"}
end
end
def validate_thinking(other),
do: {:error, "expected :adaptive, :disabled, or {:enabled, budget_tokens: pos_integer}, got: #{inspect(other)}"}
@doc false
def validate_sandbox(%ClaudeCode.Sandbox{} = sandbox), do: {:ok, sandbox}
def validate_sandbox(opts) when is_list(opts) do
{:ok, ClaudeCode.Sandbox.new(opts)}
end
def validate_sandbox(opts) when is_map(opts) do
{:ok, ClaudeCode.Sandbox.new(opts)}
end
def validate_sandbox(other) do
{:error, "expected a %ClaudeCode.Sandbox{} struct, keyword list, or map, got: #{inspect(other)}"}
end
@doc false
def validate_can_use_tool(callback) when is_function(callback, 2), do: {:ok, callback}
def validate_can_use_tool(module) when is_atom(module) do
if Code.ensure_loaded?(module) and function_exported?(module, :call, 2) do
{:ok, module}
else
{:error, "expected a module implementing ClaudeCode.Hook (call/2), got: #{inspect(module)}"}
end
end
def validate_can_use_tool(other) do
{:error, "expected a module implementing ClaudeCode.Hook or a 2-arity function, got: #{inspect(other)}"}
end
defp validate_mutual_exclusions(opts) do
if Keyword.get(opts, :can_use_tool) && Keyword.get(opts, :permission_prompt_tool) do
{:error,
%NimbleOptions.ValidationError{
key: :can_use_tool,
message: ":can_use_tool and :permission_prompt_tool are mutually exclusive — use one or the other"
}}
else
{:ok, opts}
end
end
defp warn_deprecated_max_thinking_tokens(opts) do
if Keyword.has_key?(opts, :max_thinking_tokens) && !Keyword.has_key?(opts, :thinking) do
Logger.warning(
":max_thinking_tokens is deprecated, use thinking: :adaptive | :disabled | {:enabled, budget_tokens: N} instead"
)
end
end
defp normalize_agents(opts) do
case Keyword.get(opts, :agents) do
[%ClaudeCode.Agent{} | _] = agents ->
Keyword.put(opts, :agents, ClaudeCode.Agent.to_agents_map(agents))
_ ->
opts
end
end
end