claude-agent-sdk-rust/README.md
Sulkta d312317249
Some checks failed
ci / check (push) Failing after 1s
license: relicense to AGPL-3.0-or-later
2026-06-28 21:17:30 -07:00

7.5 KiB

claude-agent-sdk (Rust)

Async Rust SDK for the Claude Agent CLI — a faithful port of the official Python claude-agent-sdk.

This crate wraps the claude CLI as a subprocess and exposes its newline-delimited JSON stream as ergonomic, strongly-typed Rust values. The Python SDK's two entry points map directly:

  • query() for fire-and-forget one-shot prompts.
  • ClaudeSDKClientclaude_agent_sdk::Client for bidirectional, multi-turn sessions.

It is an independent, community-maintained port and is not affiliated with or endorsed by Anthropic.

Status

v0.0.1 — initial port. Core feature parity for the subprocess wire protocol; advanced features (control protocol, hooks, in-process MCP servers, session store, sandbox, plugins, agents) are deferred. See scope and known limitations below.

Requirements

  • Rust 1.85 or newer (the crate uses the 2024 edition).
  • The claude CLI available on PATH (or supplied explicitly via ClaudeAgentOptions::with_cli_path()), and an authenticated Claude session. The SDK does not bundle the CLI binary — it shells out to whatever claude it finds.

Install

The crate is distributed as a git dependency:

[dependencies]
claude-agent-sdk = { git = "https://git.sulkta.com/Sulkta-OSS/claude-agent-sdk-rust" }
tokio = { version = "1", features = ["full"] }
tokio-stream = "0.1"

Pin to a tag or commit (rev = "..." / tag = "...") for reproducible builds.

Quick start

use claude_agent_sdk::{query, ClaudeAgentOptions, Message, ContentBlock};
use tokio_stream::StreamExt;

#[tokio::main]
async fn main() -> claude_agent_sdk::Result<()> {
    let opts = ClaudeAgentOptions::new()
        .with_system_prompt("You are a helpful assistant.")
        .with_max_turns(1);

    let mut stream = query("What is 2 + 2?", opts).await?;

    while let Some(msg) = stream.next().await {
        match msg? {
            Message::Assistant(a) => {
                for block in a.message.content {
                    if let ContentBlock::Text(t) = block {
                        println!("Claude: {}", t.text);
                    }
                }
            }
            Message::Result(r) => {
                if let Some(usd) = r.total_cost_usd {
                    println!("Cost: ${:.4}", usd);
                }
                break;
            }
            _ => {}
        }
    }
    Ok(())
}

query() returns a stream that ends when the CLI emits its terminal result message, automatically tearing down the subprocess.

Multi-turn sessions

use claude_agent_sdk::{Client, ClaudeAgentOptions, Message, ContentBlock};
use tokio_stream::StreamExt;

#[tokio::main]
async fn main() -> claude_agent_sdk::Result<()> {
    let mut client = Client::new(ClaudeAgentOptions::new()).await?;
    client.connect().await?;
    let mut messages = client.messages();

    client.send("What's the capital of France?").await?;
    // ...consume messages until a Result frame, then ask a follow-up...
    client.send("And of Germany?").await?;

    // Drain until the next Result, then disconnect.
    while let Some(msg) = messages.next().await {
        if let Message::Result(_) = msg? {
            break;
        }
    }
    client.disconnect().await?;
    Ok(())
}

Client keeps a single CLI subprocess alive across turns, so follow-up prompts don't pay the cold-start cost again.

Configuration

Both entry points are configured with ClaudeAgentOptions, a builder you chain .with_*() methods on:

use claude_agent_sdk::{ClaudeAgentOptions, PermissionMode};

let opts = ClaudeAgentOptions::new()
    .with_model("claude-sonnet-4-5")
    .with_permission_mode(PermissionMode::AcceptEdits)
    .with_allowed_tool("Read")
    .with_allowed_tool("Bash")
    .with_cwd("/tmp/my-project")
    .with_max_turns(5);

See the ClaudeAgentOptions API docs for the full list of supported fields.

Examples

Runnable examples live in examples/. With the claude CLI on PATH and authenticated:

cargo run --example basic         # minimal one-shot query()
cargo run --example with_options  # query() with a configured options builder
cargo run --example interactive   # multi-turn Client session

Building and testing

cargo build
cargo test            # unit + end-to-end tests. A fake CLI fixture stands in
                      # for the real `claude` binary, so the suite needs no
                      # network access or authenticated session to run.
cargo clippy --all-targets
cargo fmt --check

Mapping the Python SDK

Python Rust
query(prompt=...) query(prompt, opts).await?
ClaudeSDKClient Client
ClaudeAgentOptions ClaudeAgentOptions
AssistantMessage Message::Assistant(_)
UserMessage Message::User(_)
SystemMessage Message::System(_)
ResultMessage Message::Result(_)
TextBlock / ToolUseBlock ContentBlock::Text/ToolUse(_)
CLINotFoundError Error::CliNotFound
CLIConnectionError Error::CliConnection
ProcessError Error::Process { .. }
CLIJSONDecodeError Error::JsonDecode { .. }
MessageParseError Error::MessageParse { .. }

Scope and known limitations

This release covers the core path of the Python SDK:

  • Subprocess transport with newline-delimited JSON framing.
  • All message and content-block types parsed faithfully.
  • query() + Client for one-shot and bidirectional use.
  • The ClaudeAgentOptions flag surface relevant to subprocess arguments.

The following are deferred — see the upstream Python README for the current shape of these features:

  • Control protocol over JSON-RPCinterrupt(), set_permission_mode(), set_model(), get_mcp_status(), etc. The Rust Client currently speaks only the bare user / assistant / result frames. Adding the control protocol unlocks the rest of the API.
  • can_use_tool permission callback — requires the control protocol.
  • In-process MCP servers (@tool / create_sdk_mcp_server) — needs a Rust trait/derive-macro shape that isn't drafted yet.
  • HookMatcher — the wire format is supported on the CLI side, but the Rust callback surface is not designed.
  • SessionStore mirroring adapter.
  • Sandbox settings, plugins, agents dataclass.

Each of these degrades gracefully — a caller using a newer CLI sees fewer features, not breakage.

Contributing

Contributions are welcome. A good change:

  • keeps the wire protocol faithful to the upstream Python SDK (cite the behavior you're matching where it isn't obvious);
  • comes with a test — the end-to-end suite drives a fake CLI over the real framing, so most behavior can be covered without a live claude binary;
  • passes cargo fmt --check, cargo clippy --all-targets, and cargo test.

Open an issue to discuss larger features (especially anything from the deferred list above) before sending a big patch.

License

AGPL-3.0-or-later. See LICENSE.

This is an independent port; the upstream Python SDK is © Anthropic, PBC. This port is licensed under the GNU Affero General Public License v3.0 or later.