use crate::control::handlers::CanUseToolHandler;
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::path::PathBuf;
use std::sync::Arc;
pub struct SdkBeta;
impl SdkBeta {
pub const INTERLEAVED_THINKING: &'static str = "interleaved-thinking-2025-05-14";
pub const CONTEXT_1M: &'static str = "context-1m-2025-08-07";
pub const MAX_TOKENS_65K: &'static str = "max-tokens-3-5-sonnet-2024-07-15";
pub const COMPUTER_USE: &'static str = "computer-use-2024-10-22";
pub const MESSAGE_BATCHES: &'static str = "message-batches-2024-09-24";
pub const FILES_API: &'static str = "files-api-2025-04-14";
}
#[derive(Debug, Clone)]
pub enum SystemPrompt {
Custom(String),
Preset {
preset: String,
},
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum PermissionMode {
Default,
AcceptEdits,
BypassPermissions,
Plan,
Allow,
Ask,
Deny,
Custom,
}
impl PermissionMode {
pub fn to_cli_arg(&self) -> &str {
match self {
PermissionMode::Default => "default",
PermissionMode::AcceptEdits => "acceptEdits",
PermissionMode::BypassPermissions => "bypassPermissions",
PermissionMode::Plan => "plan",
PermissionMode::Allow => "allow",
PermissionMode::Ask => "ask",
PermissionMode::Deny => "deny",
PermissionMode::Custom => "custom",
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct McpStdioServerConfig {
pub command: String,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub args: Vec<String>,
#[serde(default, skip_serializing_if = "HashMap::is_empty")]
pub env: HashMap<String, String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct McpSSEServerConfig {
pub url: String,
#[serde(default, skip_serializing_if = "HashMap::is_empty")]
pub headers: HashMap<String, String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub timeout: Option<f64>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct McpHttpServerConfig {
pub url: String,
#[serde(default, skip_serializing_if = "HashMap::is_empty")]
pub headers: HashMap<String, String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub timeout: Option<f64>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(tag = "type", rename_all = "lowercase")]
pub enum McpServerConfig {
Stdio(McpStdioServerConfig),
Sse(McpSSEServerConfig),
Http(McpHttpServerConfig),
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SdkMcpServer {
pub name: String,
pub version: String,
}
#[derive(Debug, Clone, Hash, Eq, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "PascalCase")]
pub enum HookEvent {
PreToolUse,
PostToolUse,
PostToolUseFailure,
UserPromptSubmit,
Stop,
SubagentStop,
SubagentStart,
PreCompact,
Notification,
PermissionRequest,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct HookMatcher {
#[serde(skip_serializing_if = "Option::is_none")]
pub tool_name: Option<String>,
#[serde(skip_serializing_if = "Vec::is_empty", default)]
pub hooks: Vec<HookEvent>,
#[serde(skip_serializing_if = "Option::is_none")]
pub timeout_ms: Option<u64>,
}
impl HookMatcher {
pub fn all() -> Self {
Self {
tool_name: None,
hooks: Vec::new(),
timeout_ms: None,
}
}
pub fn tool(name: impl Into<String>) -> Self {
Self {
tool_name: Some(name.into()),
hooks: Vec::new(),
timeout_ms: None,
}
}
pub fn with_events(mut self, events: Vec<HookEvent>) -> Self {
self.hooks = events;
self
}
pub fn with_timeout_ms(mut self, ms: u64) -> Self {
self.timeout_ms = Some(ms);
self
}
pub fn matches(&self, tool_name: &str) -> bool {
match &self.tool_name {
None => true, Some(pattern) => match_glob_pattern(pattern, tool_name),
}
}
pub fn matches_event(&self, event: &HookEvent) -> bool {
self.hooks.is_empty() || self.hooks.contains(event)
}
}
fn match_glob_pattern(pattern: &str, value: &str) -> bool {
let parts: Vec<&str> = pattern.split('*').collect();
if parts.len() == 1 {
return pattern == value;
}
let mut remaining = value;
for (i, part) in parts.iter().enumerate() {
if part.is_empty() {
continue;
}
if i == 0 {
if !remaining.starts_with(part) {
return false;
}
remaining = &remaining[part.len()..];
} else if i == parts.len() - 1 {
if !remaining.ends_with(part) {
return false;
}
} else {
match remaining.find(part) {
Some(pos) => remaining = &remaining[pos + part.len()..],
None => return false,
}
}
}
true
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AgentDefinition {
pub description: String,
pub prompt: String,
pub tools: Vec<String>,
pub model: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SandboxSettings {
pub enabled: bool,
pub auto_allow_bash_if_sandboxed: bool,
pub excluded_commands: Vec<String>,
pub allow_unsandboxed_commands: Vec<String>,
pub network: Option<bool>,
}
impl SandboxSettings {
pub fn enabled() -> Self {
Self {
enabled: true,
auto_allow_bash_if_sandboxed: false,
excluded_commands: vec![],
allow_unsandboxed_commands: vec![],
network: None,
}
}
pub fn disabled() -> Self {
Self {
enabled: false,
auto_allow_bash_if_sandboxed: false,
excluded_commands: vec![],
allow_unsandboxed_commands: vec![],
network: None,
}
}
}
#[derive(Default)]
pub struct ClaudeAgentOptions {
pub system_prompt: Option<SystemPrompt>,
pub append_system_prompt: Option<String>,
pub max_turns: Option<u32>,
pub model: Option<String>,
pub allowed_tools: Vec<String>,
pub disallowed_tools: Vec<String>,
pub permission_mode: Option<PermissionMode>,
pub permission_prompt_tool_allowlist: Vec<String>,
pub mcp_servers: HashMap<String, McpServerConfig>,
pub sdk_mcp_servers: Vec<SdkMcpServer>,
pub hooks: HashMap<HookEvent, Vec<HookMatcher>>,
pub agents: HashMap<String, AgentDefinition>,
pub resume: Option<String>,
pub continue_conversation: bool,
pub fork_session: bool,
pub session_name: Option<String>,
pub enable_file_checkpointing: bool,
pub cwd: Option<PathBuf>,
pub cli_path: Option<PathBuf>,
pub env: HashMap<String, String>,
pub setting_sources: Option<Vec<String>>,
pub output_format: Option<serde_json::Value>,
pub include_partial_messages: bool,
pub betas: Vec<String>,
pub sandbox_settings: Option<SandboxSettings>,
pub max_budget_usd: Option<f64>,
pub max_thinking_tokens: Option<u64>,
pub add_dirs: Vec<PathBuf>,
pub user: Option<String>,
pub fallback_model: Option<String>,
pub max_buffer_size: Option<usize>,
pub extra_args: Vec<(String, Option<String>)>,
#[allow(clippy::type_complexity)]
pub stderr_callback: Option<std::sync::Arc<dyn Fn(String) + Send + Sync>>,
pub permission_handler: Option<Arc<dyn CanUseToolHandler>>,
}
impl std::fmt::Debug for ClaudeAgentOptions {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("ClaudeAgentOptions")
.field("system_prompt", &self.system_prompt)
.field("append_system_prompt", &self.append_system_prompt)
.field("max_turns", &self.max_turns)
.field("model", &self.model)
.field("allowed_tools", &self.allowed_tools)
.field("disallowed_tools", &self.disallowed_tools)
.field("permission_mode", &self.permission_mode)
.field(
"permission_prompt_tool_allowlist",
&self.permission_prompt_tool_allowlist,
)
.field("mcp_servers", &"<McpServerConfig map>")
.field("sdk_mcp_servers", &self.sdk_mcp_servers)
.field("hooks", &self.hooks)
.field("agents", &self.agents)
.field("resume", &self.resume)
.field("fork_session", &self.fork_session)
.field("continue_conversation", &self.continue_conversation)
.field("session_name", &self.session_name)
.field("enable_file_checkpointing", &self.enable_file_checkpointing)
.field("cwd", &self.cwd)
.field("cli_path", &self.cli_path)
.field("env", &self.env)
.field("setting_sources", &self.setting_sources)
.field("output_format", &self.output_format)
.field("include_partial_messages", &self.include_partial_messages)
.field("betas", &self.betas)
.field("sandbox_settings", &self.sandbox_settings)
.field("max_budget_usd", &self.max_budget_usd)
.field("max_thinking_tokens", &self.max_thinking_tokens)
.field("add_dirs", &self.add_dirs)
.field("user", &self.user)
.field("fallback_model", &self.fallback_model)
.field("max_buffer_size", &self.max_buffer_size)
.field("extra_args", &self.extra_args)
.field(
"stderr_callback",
&self.stderr_callback.as_ref().map(|_| "<Fn(String)>"),
)
.field(
"permission_handler",
&self
.permission_handler
.as_ref()
.map(|_| "<CanUseToolHandler>"),
)
.finish()
}
}
impl Clone for ClaudeAgentOptions {
fn clone(&self) -> Self {
Self {
system_prompt: self.system_prompt.clone(),
append_system_prompt: self.append_system_prompt.clone(),
max_turns: self.max_turns,
model: self.model.clone(),
allowed_tools: self.allowed_tools.clone(),
disallowed_tools: self.disallowed_tools.clone(),
permission_mode: self.permission_mode.clone(),
permission_prompt_tool_allowlist: self.permission_prompt_tool_allowlist.clone(),
mcp_servers: self.mcp_servers.clone(),
sdk_mcp_servers: self.sdk_mcp_servers.clone(),
hooks: self.hooks.clone(),
agents: self.agents.clone(),
resume: self.resume.clone(),
fork_session: self.fork_session,
continue_conversation: self.continue_conversation,
session_name: self.session_name.clone(),
enable_file_checkpointing: self.enable_file_checkpointing,
cwd: self.cwd.clone(),
cli_path: self.cli_path.clone(),
env: self.env.clone(),
setting_sources: self.setting_sources.clone(),
output_format: self.output_format.clone(),
include_partial_messages: self.include_partial_messages,
betas: self.betas.clone(),
sandbox_settings: self.sandbox_settings.clone(),
max_budget_usd: self.max_budget_usd,
max_thinking_tokens: self.max_thinking_tokens,
add_dirs: self.add_dirs.clone(),
user: self.user.clone(),
fallback_model: self.fallback_model.clone(),
max_buffer_size: self.max_buffer_size,
extra_args: self.extra_args.clone(),
stderr_callback: self.stderr_callback.clone(),
permission_handler: self.permission_handler.clone(),
}
}
}
impl ClaudeAgentOptions {
pub fn builder() -> ClaudeAgentOptionsBuilder {
ClaudeAgentOptionsBuilder::default()
}
pub fn to_mcp_config_json(&self) -> Result<Option<String>, serde_json::Error> {
if self.mcp_servers.is_empty() {
return Ok(None);
}
let payload = serde_json::json!({ "mcpServers": &self.mcp_servers });
Ok(Some(serde_json::to_string(&payload)?))
}
pub fn to_base_cli_args(&self) -> Vec<String> {
let mut args = vec![
"--output-format".to_string(),
"stream-json".to_string(),
"--verbose".to_string(),
];
if let Some(max_turns) = self.max_turns {
args.push("--max-turns".to_string());
args.push(max_turns.to_string());
}
if let Some(model) = &self.model {
args.push("--model".to_string());
args.push(model.clone());
}
if let Some(mode) = &self.permission_mode {
args.push("--permission-mode".to_string());
args.push(mode.to_cli_arg().to_string());
}
if let Some(sys_prompt) = &self.system_prompt {
match sys_prompt {
SystemPrompt::Custom(text) => {
args.push("--system-prompt".to_string());
args.push(text.clone());
}
SystemPrompt::Preset { preset } => {
args.push("--system-prompt-preset".to_string());
args.push(preset.clone());
}
}
}
if let Some(append) = &self.append_system_prompt {
args.push("--append-system-prompt".to_string());
args.push(append.clone());
}
if !self.allowed_tools.is_empty() {
args.push("--allowed-tools".to_string());
args.push(self.allowed_tools.join(","));
}
if !self.disallowed_tools.is_empty() {
args.push("--disallowed-tools".to_string());
args.push(self.disallowed_tools.join(","));
}
for tool in &self.permission_prompt_tool_allowlist {
args.push("--permission-prompt-tool-name".to_string());
args.push(tool.clone());
}
for beta in &self.betas {
args.push("--beta".to_string());
args.push(beta.clone());
}
if let Some(resume) = &self.resume {
args.push("--resume".to_string());
args.push(resume.clone());
}
if self.continue_conversation {
args.push("--continue".to_string());
}
if self.fork_session {
args.push("--fork-session".to_string());
}
if let Some(name) = &self.session_name {
args.push("--session-name".to_string());
args.push(name.clone());
}
if self.enable_file_checkpointing {
args.push("--enable-file-checkpointing".to_string());
}
match &self.setting_sources {
Some(sources) => {
args.push("--setting-sources".to_string());
args.push(sources.join(","));
}
None => {
args.push("--setting-sources".to_string());
args.push(String::new());
}
}
if let Some(max_budget) = self.max_budget_usd {
args.push("--max-budget-usd".to_string());
args.push(max_budget.to_string());
}
if let Some(max_thinking) = self.max_thinking_tokens {
args.push("--max-thinking-tokens".to_string());
args.push(max_thinking.to_string());
}
if let Some(sandbox) = &self.sandbox_settings {
if sandbox.enabled {
args.push("--sandbox".to_string());
} else {
args.push("--no-sandbox".to_string());
}
if sandbox.auto_allow_bash_if_sandboxed {
args.push("--auto-allow-bash-if-sandboxed".to_string());
}
for cmd in &sandbox.excluded_commands {
args.push("--excluded-command".to_string());
args.push(cmd.clone());
}
for cmd in &sandbox.allow_unsandboxed_commands {
args.push("--allow-unsandboxed-command".to_string());
args.push(cmd.clone());
}
if let Some(network) = sandbox.network {
if network {
args.push("--sandbox-network".to_string());
} else {
args.push("--no-sandbox-network".to_string());
}
}
}
for dir in &self.add_dirs {
args.push("--add-dir".to_string());
args.push(dir.to_string_lossy().into_owned());
}
if let Some(user) = &self.user {
args.push("--user".to_string());
args.push(user.clone());
}
if let Some(fallback) = &self.fallback_model {
args.push("--fallback-model".to_string());
args.push(fallback.clone());
}
for (key, value) in &self.extra_args {
let flag = if key.starts_with("--") {
key.clone()
} else {
format!("--{}", key)
};
args.push(flag);
if let Some(val) = value {
args.push(val.clone());
}
}
if let Ok(Some(mcp_json)) = self.to_mcp_config_json() {
args.push("--mcp-config".to_string());
args.push(mcp_json);
}
args
}
pub fn to_cli_args(&self, prompt: &str) -> Vec<String> {
let mut args = self.to_base_cli_args();
args.push("-p".to_string());
args.push(prompt.to_string());
args
}
}
#[derive(Debug, Default)]
pub struct ClaudeAgentOptionsBuilder {
inner: ClaudeAgentOptions,
}
impl ClaudeAgentOptionsBuilder {
pub fn system_prompt(mut self, prompt: SystemPrompt) -> Self {
self.inner.system_prompt = Some(prompt);
self
}
pub fn append_system_prompt(mut self, text: impl Into<String>) -> Self {
self.inner.append_system_prompt = Some(text.into());
self
}
pub fn max_turns(mut self, turns: u32) -> Self {
self.inner.max_turns = Some(turns);
self
}
pub fn model(mut self, model: impl Into<String>) -> Self {
self.inner.model = Some(model.into());
self
}
pub fn allowed_tools(mut self, tools: Vec<String>) -> Self {
self.inner.allowed_tools = tools;
self
}
pub fn disallowed_tools(mut self, tools: Vec<String>) -> Self {
self.inner.disallowed_tools = tools;
self
}
pub fn permission_mode(mut self, mode: PermissionMode) -> Self {
self.inner.permission_mode = Some(mode);
self
}
pub fn permission_prompt_tool_allowlist(mut self, tools: Vec<String>) -> Self {
self.inner.permission_prompt_tool_allowlist = tools;
self
}
pub fn mcp_servers(mut self, servers: HashMap<String, McpServerConfig>) -> Self {
self.inner.mcp_servers = servers;
self
}
pub fn sdk_mcp_servers(mut self, servers: Vec<SdkMcpServer>) -> Self {
self.inner.sdk_mcp_servers = servers;
self
}
pub fn hooks(mut self, hooks: HashMap<HookEvent, Vec<HookMatcher>>) -> Self {
self.inner.hooks = hooks;
self
}
pub fn agents(mut self, agents: HashMap<String, AgentDefinition>) -> Self {
self.inner.agents = agents;
self
}
pub fn resume(mut self, session_id: impl Into<String>) -> Self {
self.inner.resume = Some(session_id.into());
self
}
pub fn continue_conversation(mut self, continue_conv: bool) -> Self {
self.inner.continue_conversation = continue_conv;
self
}
pub fn fork_session(mut self, fork: bool) -> Self {
self.inner.fork_session = fork;
self
}
pub fn session_name(mut self, name: impl Into<String>) -> Self {
self.inner.session_name = Some(name.into());
self
}
pub fn enable_file_checkpointing(mut self, enable: bool) -> Self {
self.inner.enable_file_checkpointing = enable;
self
}
pub fn cwd(mut self, path: impl Into<PathBuf>) -> Self {
self.inner.cwd = Some(path.into());
self
}
pub fn cli_path(mut self, path: impl Into<PathBuf>) -> Self {
self.inner.cli_path = Some(path.into());
self
}
pub fn env(mut self, env: HashMap<String, String>) -> Self {
self.inner.env = env;
self
}
pub fn setting_sources(mut self, sources: Vec<String>) -> Self {
self.inner.setting_sources = Some(sources);
self
}
#[deprecated(
since = "0.4.0",
note = "Renamed to `setting_sources` to match Python SDK"
)]
#[allow(deprecated)]
pub fn settings_sources(mut self, sources: Vec<String>) -> Self {
self.inner.setting_sources = Some(sources);
self
}
pub fn output_format(mut self, format: serde_json::Value) -> Self {
self.inner.output_format = Some(format);
self
}
pub fn include_partial_messages(mut self, include: bool) -> Self {
self.inner.include_partial_messages = include;
self
}
pub fn betas(mut self, betas: Vec<String>) -> Self {
self.inner.betas = betas;
self
}
pub fn sandbox_settings(mut self, settings: SandboxSettings) -> Self {
self.inner.sandbox_settings = Some(settings);
self
}
pub fn max_budget_usd(mut self, budget: f64) -> Self {
self.inner.max_budget_usd = Some(budget);
self
}
pub fn max_thinking_tokens(mut self, tokens: u64) -> Self {
self.inner.max_thinking_tokens = Some(tokens);
self
}
pub fn add_dirs(mut self, dirs: impl IntoIterator<Item = impl Into<PathBuf>>) -> Self {
self.inner.add_dirs = dirs.into_iter().map(Into::into).collect();
self
}
pub fn add_dir(mut self, dir: impl Into<PathBuf>) -> Self {
self.inner.add_dirs.push(dir.into());
self
}
pub fn user(mut self, user: impl Into<String>) -> Self {
self.inner.user = Some(user.into());
self
}
pub fn fallback_model(mut self, model: impl Into<String>) -> Self {
self.inner.fallback_model = Some(model.into());
self
}
pub fn max_buffer_size(mut self, size: usize) -> Self {
self.inner.max_buffer_size = Some(size);
self
}
pub fn extra_args(mut self, args: impl IntoIterator<Item = (String, Option<String>)>) -> Self {
self.inner.extra_args = args.into_iter().collect();
self
}
pub fn extra_arg(mut self, key: impl Into<String>, value: Option<String>) -> Self {
self.inner.extra_args.push((key.into(), value));
self
}
pub fn stderr_callback(mut self, callback: impl Fn(String) + Send + Sync + 'static) -> Self {
self.inner.stderr_callback = Some(std::sync::Arc::new(callback));
self
}
pub fn permission_handler(mut self, handler: impl CanUseToolHandler + 'static) -> Self {
self.inner.permission_handler = Some(Arc::new(handler));
self
}
pub fn build(self) -> ClaudeAgentOptions {
self.inner
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_builder_default() {
let opts = ClaudeAgentOptions::builder().build();
assert_eq!(opts.max_turns, None);
assert_eq!(opts.model, None);
assert!(opts.allowed_tools.is_empty());
assert!(opts.disallowed_tools.is_empty());
assert!(opts.mcp_servers.is_empty());
assert!(opts.hooks.is_empty());
assert!(opts.agents.is_empty());
assert!(!opts.fork_session);
assert!(!opts.continue_conversation);
assert!(!opts.enable_file_checkpointing);
assert!(!opts.include_partial_messages);
assert_eq!(opts.max_budget_usd, None);
assert_eq!(opts.max_thinking_tokens, None);
assert!(opts.sandbox_settings.is_none());
}
#[test]
fn test_builder_chaining() {
let opts = ClaudeAgentOptions::builder()
.max_turns(5)
.model("claude-sonnet-4")
.allowed_tools(vec!["Read".to_string(), "Bash".to_string()])
.permission_mode(PermissionMode::AcceptEdits)
.build();
assert_eq!(opts.max_turns, Some(5));
assert_eq!(opts.model, Some("claude-sonnet-4".to_string()));
assert_eq!(opts.allowed_tools.len(), 2);
assert!(matches!(
opts.permission_mode,
Some(PermissionMode::AcceptEdits)
));
}
#[test]
fn test_builder_all_fields() {
let opts = ClaudeAgentOptions::builder()
.system_prompt(SystemPrompt::Custom("test".to_string()))
.append_system_prompt("append")
.max_turns(10)
.model("claude-opus-4")
.allowed_tools(vec!["Read".to_string()])
.disallowed_tools(vec!["Bash".to_string()])
.permission_mode(PermissionMode::Plan)
.permission_prompt_tool_allowlist(vec!["Edit".to_string()])
.resume("session-123")
.continue_conversation(true)
.fork_session(true)
.session_name("test-session")
.enable_file_checkpointing(true)
.cwd("/tmp")
.include_partial_messages(true)
.betas(vec!["feature-1".to_string()])
.max_budget_usd(5.0)
.max_thinking_tokens(10000)
.build();
assert!(opts.system_prompt.is_some());
assert_eq!(opts.append_system_prompt, Some("append".to_string()));
assert_eq!(opts.max_turns, Some(10));
assert_eq!(opts.model, Some("claude-opus-4".to_string()));
assert_eq!(opts.allowed_tools, vec!["Read".to_string()]);
assert_eq!(opts.disallowed_tools, vec!["Bash".to_string()]);
assert!(matches!(opts.permission_mode, Some(PermissionMode::Plan)));
assert_eq!(
opts.permission_prompt_tool_allowlist,
vec!["Edit".to_string()]
);
assert_eq!(opts.resume, Some("session-123".to_string()));
assert!(opts.continue_conversation);
assert!(opts.fork_session);
assert_eq!(opts.session_name, Some("test-session".to_string()));
assert!(opts.enable_file_checkpointing);
assert!(opts.cwd.is_some());
assert!(opts.include_partial_messages);
assert_eq!(opts.betas, vec!["feature-1".to_string()]);
assert_eq!(opts.max_budget_usd, Some(5.0));
assert_eq!(opts.max_thinking_tokens, Some(10000));
}
#[test]
fn test_to_cli_args_minimal() {
let opts = ClaudeAgentOptions::default();
let args = opts.to_cli_args("test prompt");
assert!(args.contains(&"--output-format".to_string()));
assert!(args.contains(&"stream-json".to_string()));
assert!(args.contains(&"--verbose".to_string()));
assert!(args.contains(&"--setting-sources".to_string()));
assert!(args.contains(&"-p".to_string()));
assert!(args.contains(&"test prompt".to_string()));
}
#[test]
fn test_to_cli_args_with_options() {
let opts = ClaudeAgentOptions::builder()
.max_turns(10)
.model("claude-opus-4")
.permission_mode(PermissionMode::Plan)
.build();
let args = opts.to_cli_args("test");
assert!(args.contains(&"--max-turns".to_string()));
assert!(args.contains(&"10".to_string()));
assert!(args.contains(&"--model".to_string()));
assert!(args.contains(&"claude-opus-4".to_string()));
assert!(args.contains(&"--permission-mode".to_string()));
assert!(args.contains(&"plan".to_string()));
}
#[test]
fn test_to_cli_args_system_prompt_custom() {
let opts = ClaudeAgentOptions::builder()
.system_prompt(SystemPrompt::Custom("You are a helper".to_string()))
.build();
let args = opts.to_cli_args("test");
assert!(args.contains(&"--system-prompt".to_string()));
assert!(args.contains(&"You are a helper".to_string()));
}
#[test]
fn test_to_cli_args_system_prompt_preset() {
let opts = ClaudeAgentOptions::builder()
.system_prompt(SystemPrompt::Preset {
preset: "assistant".to_string(),
})
.build();
let args = opts.to_cli_args("test");
assert!(args.contains(&"--system-prompt-preset".to_string()));
assert!(args.contains(&"assistant".to_string()));
}
#[test]
fn test_to_cli_args_allowed_tools() {
let opts = ClaudeAgentOptions::builder()
.allowed_tools(vec!["Read".to_string(), "Bash".to_string()])
.build();
let args = opts.to_cli_args("test");
assert!(args.contains(&"--allowed-tools".to_string()));
assert!(args.contains(&"Read,Bash".to_string()));
}
#[test]
fn test_to_cli_args_disallowed_tools() {
let opts = ClaudeAgentOptions::builder()
.disallowed_tools(vec!["Edit".to_string(), "Write".to_string()])
.build();
let args = opts.to_cli_args("test");
assert!(args.contains(&"--disallowed-tools".to_string()));
assert!(args.contains(&"Edit,Write".to_string()));
}
#[test]
fn test_to_cli_args_session_options() {
let opts = ClaudeAgentOptions::builder()
.resume("session-123")
.fork_session(true)
.session_name("my-session")
.enable_file_checkpointing(true)
.build();
let args = opts.to_cli_args("test");
assert!(args.contains(&"--resume".to_string()));
assert!(args.contains(&"session-123".to_string()));
assert!(args.contains(&"--fork-session".to_string()));
assert!(args.contains(&"--session-name".to_string()));
assert!(args.contains(&"my-session".to_string()));
assert!(args.contains(&"--enable-file-checkpointing".to_string()));
}
#[test]
fn test_to_cli_args_setting_sources_default() {
let opts = ClaudeAgentOptions::default();
let args = opts.to_cli_args("test");
assert!(args.contains(&"--setting-sources".to_string()));
let idx = args.iter().position(|a| a == "--setting-sources").unwrap();
assert_eq!(args[idx + 1], "");
}
#[test]
fn test_to_cli_args_setting_sources_custom() {
let opts = ClaudeAgentOptions::builder()
.setting_sources(vec!["local".to_string(), "project".to_string()])
.build();
let args = opts.to_cli_args("test");
assert!(args.contains(&"--setting-sources".to_string()));
assert!(args.contains(&"local,project".to_string()));
}
#[test]
fn test_permission_mode_to_cli_arg() {
assert_eq!(PermissionMode::Default.to_cli_arg(), "default");
assert_eq!(PermissionMode::AcceptEdits.to_cli_arg(), "acceptEdits");
assert_eq!(
PermissionMode::BypassPermissions.to_cli_arg(),
"bypassPermissions"
);
assert_eq!(PermissionMode::Plan.to_cli_arg(), "plan");
}
#[test]
fn test_default_trait() {
let opts = ClaudeAgentOptions::default();
assert!(opts.system_prompt.is_none());
assert!(opts.max_turns.is_none());
assert!(opts.model.is_none());
assert!(opts.allowed_tools.is_empty());
assert!(opts.permission_mode.is_none());
assert!(opts.resume.is_none());
assert!(!opts.fork_session);
assert!(!opts.continue_conversation);
assert!(opts.max_budget_usd.is_none());
assert!(opts.max_thinking_tokens.is_none());
assert!(opts.sandbox_settings.is_none());
}
#[test]
fn test_collections_handling() {
let mut env = HashMap::new();
env.insert("KEY".to_string(), "value".to_string());
let opts = ClaudeAgentOptions::builder().env(env.clone()).build();
assert_eq!(opts.env, env);
}
#[test]
fn test_pathbuf_conversion() {
let opts = ClaudeAgentOptions::builder()
.cwd("/tmp/test")
.cli_path("/usr/bin/claude")
.build();
assert_eq!(opts.cwd, Some(PathBuf::from("/tmp/test")));
assert_eq!(opts.cli_path, Some(PathBuf::from("/usr/bin/claude")));
}
#[test]
fn test_to_cli_args_permission_prompt_tool_allowlist() {
let opts = ClaudeAgentOptions::builder()
.permission_prompt_tool_allowlist(vec!["Bash".to_string(), "Edit".to_string()])
.build();
let args = opts.to_cli_args("test");
let tool_flags: Vec<_> = args
.iter()
.enumerate()
.filter(|(_, a)| a.as_str() == "--permission-prompt-tool-name")
.map(|(i, _)| args[i + 1].as_str())
.collect();
assert_eq!(tool_flags, vec!["Bash", "Edit"]);
}
#[test]
fn test_to_cli_args_betas() {
let opts = ClaudeAgentOptions::builder()
.betas(vec![
"beta-feature-1".to_string(),
"beta-feature-2".to_string(),
])
.build();
let args = opts.to_cli_args("test");
let beta_flags: Vec<_> = args
.iter()
.enumerate()
.filter(|(_, a)| a.as_str() == "--beta")
.map(|(i, _)| args[i + 1].as_str())
.collect();
assert_eq!(beta_flags, vec!["beta-feature-1", "beta-feature-2"]);
}
#[test]
fn test_to_cli_args_continue_conversation() {
let opts = ClaudeAgentOptions::builder()
.continue_conversation(true)
.build();
let args = opts.to_cli_args("test");
assert!(args.contains(&"--continue".to_string()));
}
#[test]
fn test_to_cli_args_continue_false() {
let opts = ClaudeAgentOptions::builder()
.continue_conversation(false)
.build();
let args = opts.to_cli_args("test");
assert!(!args.contains(&"--continue".to_string()));
}
#[test]
fn test_to_cli_args_max_budget_usd() {
let opts = ClaudeAgentOptions::builder().max_budget_usd(10.5).build();
let args = opts.to_cli_args("test");
assert!(args.contains(&"--max-budget-usd".to_string()));
assert!(args.contains(&"10.5".to_string()));
}
#[test]
fn test_to_cli_args_max_thinking_tokens() {
let opts = ClaudeAgentOptions::builder()
.max_thinking_tokens(8000)
.build();
let args = opts.to_cli_args("test");
assert!(args.contains(&"--max-thinking-tokens".to_string()));
assert!(args.contains(&"8000".to_string()));
}
#[test]
fn test_to_cli_args_sandbox_enabled() {
let opts = ClaudeAgentOptions::builder()
.sandbox_settings(SandboxSettings::enabled())
.build();
let args = opts.to_cli_args("test");
assert!(args.contains(&"--sandbox".to_string()));
assert!(!args.contains(&"--no-sandbox".to_string()));
}
#[test]
fn test_to_cli_args_sandbox_disabled() {
let opts = ClaudeAgentOptions::builder()
.sandbox_settings(SandboxSettings::disabled())
.build();
let args = opts.to_cli_args("test");
assert!(args.contains(&"--no-sandbox".to_string()));
assert!(!args.contains(&"--sandbox".to_string()));
}
#[test]
fn test_to_cli_args_sandbox_full_settings() {
let settings = SandboxSettings {
enabled: true,
auto_allow_bash_if_sandboxed: true,
excluded_commands: vec!["rm".to_string(), "sudo".to_string()],
allow_unsandboxed_commands: vec!["git".to_string()],
network: Some(false),
};
let opts = ClaudeAgentOptions::builder()
.sandbox_settings(settings)
.build();
let args = opts.to_cli_args("test");
assert!(args.contains(&"--sandbox".to_string()));
assert!(args.contains(&"--auto-allow-bash-if-sandboxed".to_string()));
assert!(args.contains(&"--excluded-command".to_string()));
assert!(args.contains(&"rm".to_string()));
assert!(args.contains(&"sudo".to_string()));
assert!(args.contains(&"--allow-unsandboxed-command".to_string()));
assert!(args.contains(&"git".to_string()));
assert!(args.contains(&"--no-sandbox-network".to_string()));
}
#[test]
fn test_to_cli_args_sandbox_network_allow() {
let settings = SandboxSettings {
enabled: true,
auto_allow_bash_if_sandboxed: false,
excluded_commands: vec![],
allow_unsandboxed_commands: vec![],
network: Some(true),
};
let opts = ClaudeAgentOptions::builder()
.sandbox_settings(settings)
.build();
let args = opts.to_cli_args("test");
assert!(args.contains(&"--sandbox-network".to_string()));
assert!(!args.contains(&"--no-sandbox-network".to_string()));
}
#[test]
fn test_sandbox_settings_constructors() {
let enabled = SandboxSettings::enabled();
assert!(enabled.enabled);
assert!(!enabled.auto_allow_bash_if_sandboxed);
assert!(enabled.excluded_commands.is_empty());
assert!(enabled.allow_unsandboxed_commands.is_empty());
assert!(enabled.network.is_none());
let disabled = SandboxSettings::disabled();
assert!(!disabled.enabled);
}
#[test]
fn test_to_mcp_config_json_empty() {
let opts = ClaudeAgentOptions::default();
let result = opts.to_mcp_config_json().unwrap();
assert!(result.is_none(), "Empty mcp_servers should produce None");
}
#[test]
fn test_to_cli_args_no_mcp_config_when_empty() {
let opts = ClaudeAgentOptions::default();
let args = opts.to_cli_args("test");
assert!(
!args.contains(&"--mcp-config".to_string()),
"--mcp-config should not be present when mcp_servers is empty"
);
}
#[test]
fn test_hook_matcher_all_matches_any_tool() {
let matcher = HookMatcher::all();
assert!(matcher.matches("Bash"));
assert!(matcher.matches("Read"));
assert!(matcher.matches("mcp__text_tools__word_count"));
assert!(matcher.matches(""));
}
#[test]
fn test_hook_matcher_exact_name() {
let matcher = HookMatcher::tool("Bash");
assert!(matcher.matches("Bash"));
assert!(!matcher.matches("Read"));
assert!(!matcher.matches("BashTool"));
assert!(!matcher.matches("bash")); }
#[test]
fn test_hook_matcher_wildcard_prefix() {
let matcher = HookMatcher::tool("mcp__*");
assert!(matcher.matches("mcp__text_tools__word_count"));
assert!(matcher.matches("mcp__server__tool"));
assert!(matcher.matches("mcp__"));
assert!(!matcher.matches("Bash"));
assert!(!matcher.matches("mcp_underscore")); }
#[test]
fn test_hook_matcher_wildcard_suffix() {
let matcher = HookMatcher::tool("*_tool");
assert!(matcher.matches("my_tool"));
assert!(matcher.matches("bash_tool"));
assert!(!matcher.matches("tool_runner"));
}
#[test]
fn test_hook_matcher_wildcard_only() {
let matcher = HookMatcher::tool("*");
assert!(matcher.matches("Bash"));
assert!(matcher.matches("Read"));
assert!(matcher.matches("anything"));
}
#[test]
fn test_hook_matcher_wildcard_middle() {
let matcher = HookMatcher::tool("mcp__*__tool");
assert!(matcher.matches("mcp__server__tool"));
assert!(!matcher.matches("mcp__tool")); }
#[test]
fn test_hook_matcher_timeout_ms() {
let matcher = HookMatcher::all().with_timeout_ms(5000);
assert_eq!(matcher.timeout_ms, Some(5000));
let matcher = HookMatcher::all();
assert_eq!(matcher.timeout_ms, None);
}
#[test]
fn test_hook_matcher_with_events() {
let matcher =
HookMatcher::all().with_events(vec![HookEvent::PreToolUse, HookEvent::PostToolUse]);
assert!(matcher.matches_event(&HookEvent::PreToolUse));
assert!(matcher.matches_event(&HookEvent::PostToolUse));
assert!(!matcher.matches_event(&HookEvent::Stop));
assert!(!matcher.matches_event(&HookEvent::UserPromptSubmit));
}
#[test]
fn test_hook_matcher_empty_events_matches_all() {
let matcher = HookMatcher::all(); assert!(matcher.matches_event(&HookEvent::PreToolUse));
assert!(matcher.matches_event(&HookEvent::PostToolUse));
assert!(matcher.matches_event(&HookEvent::Stop));
assert!(matcher.matches_event(&HookEvent::Notification));
}
#[test]
fn test_hook_matcher_serialization_skips_empty_hooks() {
let matcher = HookMatcher::tool("Bash");
let json = serde_json::to_value(&matcher).unwrap();
assert!(!json.as_object().unwrap().contains_key("hooks"));
assert!(!json.as_object().unwrap().contains_key("timeout_ms"));
assert_eq!(json["tool_name"], "Bash");
}
#[test]
fn test_hook_matcher_serialization_with_all_fields() {
let matcher = HookMatcher::tool("Bash")
.with_events(vec![HookEvent::PreToolUse])
.with_timeout_ms(3000);
let json = serde_json::to_value(&matcher).unwrap();
assert_eq!(json["tool_name"], "Bash");
assert_eq!(json["timeout_ms"], 3000);
assert!(json["hooks"].is_array());
}
}