Current section

Files

Jump to
claude_code lib claude_code options.ex
Raw

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