claude-code-sdk-rust 0.1.0

Async Rust SDK for the Claude Code CLI: streaming agent turns, tool use, and sessions.
Documentation
use super::cli_args::build_cli_args;
use super::transport::TransportOptions;
use crate::types::{
    ClaudeAgentOptions, PermissionMode, SandboxNetworkConfig, SandboxSettings, SdkBeta,
    ThinkingConfig, ThinkingConfigType,
};

#[test]
fn empty_tools_list_serializes_to_disable_all_tools_like_python_sdk() {
    let args = args_for(ClaudeAgentOptions::builder().tools(Vec::new()).build());

    assert_eq!(
        args.windows(2)
            .find(|window| window[0] == "--tools")
            .map(|window| window[1].as_str()),
        Some("")
    );
}

fn args_for(options: ClaudeAgentOptions) -> Vec<String> {
    build_cli_args(&TransportOptions::from(&options)).expect("args")
}

#[test]
fn serializes_permission_mode_and_betas_as_cli_wire_values() {
    let options = ClaudeAgentOptions::builder()
        .permission_mode(PermissionMode::AcceptEdits)
        .betas(vec![SdkBeta::Context1M20250807])
        .build();

    let args = args_for(options);

    assert_eq!(
        args.windows(2)
            .find(|window| window[0] == "--permission-mode")
            .map(|window| window[1].as_str()),
        Some("acceptEdits")
    );
    assert_eq!(
        args.windows(2)
            .find(|window| window[0] == "--betas")
            .map(|window| window[1].as_str()),
        Some("context-1m-2025-08-07")
    );
}

#[test]
fn serializes_thinking_and_json_schema_like_python_sdk() {
    let mut schema = serde_json::Map::new();
    schema.insert(
        "type".to_string(),
        serde_json::Value::String("object".to_string()),
    );

    let mut output_format = serde_json::Map::new();
    output_format.insert(
        "type".to_string(),
        serde_json::Value::String("json_schema".to_string()),
    );
    output_format.insert(
        "schema".to_string(),
        serde_json::Value::Object(schema.clone()),
    );
    let expected_schema = serde_json::Value::Object(schema).to_string();

    let options = ClaudeAgentOptions::builder()
        .thinking(ThinkingConfig {
            r#type: ThinkingConfigType::Adaptive,
            budget_tokens: None,
            display: Some("full".to_string()),
        })
        .output_format(output_format)
        .build();

    let args = args_for(options);

    assert_eq!(
        args.windows(2)
            .find(|window| window[0] == "--thinking")
            .map(|window| window[1].as_str()),
        Some("adaptive")
    );
    assert_eq!(
        args.windows(2)
            .find(|window| window[0] == "--thinking-display")
            .map(|window| window[1].as_str()),
        Some("full")
    );
    assert_eq!(
        args.windows(2)
            .find(|window| window[0] == "--json-schema")
            .map(|window| window[1].as_str()),
        Some(expected_schema.as_str())
    );
}

#[test]
fn disabled_thinking_does_not_forward_display() {
    let options = ClaudeAgentOptions::builder()
        .thinking(ThinkingConfig {
            r#type: ThinkingConfigType::Disabled,
            budget_tokens: None,
            display: Some("omitted".to_string()),
        })
        .build();

    let args = args_for(options);

    assert_eq!(
        args.windows(2)
            .find(|window| window[0] == "--thinking")
            .map(|window| window[1].as_str()),
        Some("disabled")
    );
    assert!(!args.iter().any(|arg| arg == "--thinking-display"));
}

#[test]
fn serializes_current_python_sdk_session_and_control_flags() {
    let options = ClaudeAgentOptions::builder()
        .session_id("session-123")
        .task_budget_total(100_000)
        .include_hook_events(true)
        .strict_mcp_config(true)
        .setting_sources(vec![
            crate::types::SettingSource::User,
            crate::types::SettingSource::Project,
        ])
        .build();

    let args = args_for(options);

    assert_eq!(
        args.windows(2)
            .find(|window| window[0] == "--session-id")
            .map(|window| window[1].as_str()),
        Some("session-123")
    );
    assert_eq!(
        args.windows(2)
            .find(|window| window[0] == "--task-budget")
            .map(|window| window[1].as_str()),
        Some("100000")
    );
    assert!(args.iter().any(|arg| arg == "--include-hook-events"));
    assert!(args.iter().any(|arg| arg == "--strict-mcp-config"));
    assert!(args
        .iter()
        .any(|arg| arg == "--setting-sources=user,project"));
}

#[test]
fn can_use_tool_defaults_permission_prompt_tool_to_stdio() {
    let options = ClaudeAgentOptions::builder()
        .can_use_tool(|_, _, _| async { Ok(crate::types::PermissionResult::allow()) })
        .build();

    let args = args_for(options);

    assert_eq!(
        args.windows(2)
            .find(|window| window[0] == "--permission-prompt-tool")
            .map(|window| window[1].as_str()),
        Some("stdio")
    );
}

#[test]
fn serializes_mcp_servers_raw_config_like_python_sdk() {
    let json_config = r#"{"mcpServers":{"server":{"type":"stdio","command":"test"}}}"#;
    let args = args_for(
        ClaudeAgentOptions::builder()
            .mcp_servers_config(json_config)
            .build(),
    );

    assert_eq!(
        args.windows(2)
            .find(|window| window[0] == "--mcp-config")
            .map(|window| window[1].as_str()),
        Some(json_config)
    );

    let path = "/path/to/mcp-config.json";
    let args = args_for(
        ClaudeAgentOptions::builder()
            .mcp_servers_config(path)
            .build(),
    );
    assert_eq!(
        args.windows(2)
            .find(|window| window[0] == "--mcp-config")
            .map(|window| window[1].as_str()),
        Some(path)
    );
}

#[test]
fn skills_config_adds_skill_tool_and_default_setting_sources() {
    let options = ClaudeAgentOptions::builder()
        .allowed_tools(vec!["Read".to_string()])
        .skills(vec!["reviewer".to_string(), "planner".to_string()])
        .build();

    let args = args_for(options);

    assert_eq!(
        args.windows(2)
            .find(|window| window[0] == "--allowedTools")
            .map(|window| window[1].as_str()),
        Some("Read,Skill(reviewer),Skill(planner)")
    );
    assert!(args
        .iter()
        .any(|arg| arg == "--setting-sources=user,project"));
}

#[test]
fn skills_all_deduplicates_skill_tool_and_respects_explicit_setting_sources() {
    let options = ClaudeAgentOptions::builder()
        .allowed_tools(vec!["Skill".to_string()])
        .skills_all()
        .setting_sources(Vec::new())
        .build();

    let args = args_for(options);

    assert_eq!(
        args.windows(2)
            .find(|window| window[0] == "--allowedTools")
            .map(|window| window[1].as_str()),
        Some("Skill")
    );
    assert!(args.iter().any(|arg| arg == "--setting-sources="));
}

#[test]
fn sandbox_settings_are_merged_into_settings_json() {
    let options = ClaudeAgentOptions::builder()
        .settings(r#"{"permissions":{"allow":["Read"]}}"#)
        .sandbox(SandboxSettings {
            enabled: Some(true),
            auto_allow_bash_if_sandboxed: Some(true),
            network: Some(SandboxNetworkConfig {
                allow_local_binding: Some(true),
                ..Default::default()
            }),
            ..Default::default()
        })
        .build();

    let args = args_for(options);
    let settings = args
        .windows(2)
        .find(|window| window[0] == "--settings")
        .map(|window| window[1].as_str())
        .expect("settings argument");
    let settings: serde_json::Value = serde_json::from_str(settings).expect("settings json");

    assert_eq!(settings["permissions"]["allow"][0], "Read");
    assert_eq!(settings["sandbox"]["enabled"], true);
    assert_eq!(
        settings["sandbox"]["autoAllowBashIfSandboxed"],
        serde_json::Value::Bool(true)
    );
    assert_eq!(settings["sandbox"]["network"]["allowLocalBinding"], true);
}

#[test]
fn unsupported_plugin_type_is_rejected() {
    let options = ClaudeAgentOptions::builder()
        .plugin(crate::types::SDKPluginConfig {
            r#type: "remote".to_string(),
            path: "/tmp/plugin".to_string(),
        })
        .build();

    let err = build_cli_args(&TransportOptions::from(&options))
        .expect_err("unsupported plugin type should fail");
    assert!(err.to_string().contains("Unsupported plugin type"));
}

#[test]
fn session_store_enables_session_mirror_flag() {
    let options = ClaudeAgentOptions::builder()
        .session_store(crate::session_store::InMemorySessionStore::new())
        .build();

    let args = args_for(options);

    assert!(args.iter().any(|arg| arg == "--session-mirror"));
}