Ports _internal/transport/subprocess_cli.py. Spawns the claude CLI with --output-format stream-json --input-format stream-json --verbose, then exposes a split (TransportReader, TransportWriter, TransportHandle) trio. The split is the key difference from the Python single-class transport: the reader half owns stdout exclusively and is moved into a background task; the writer half is Arc/Mutex over stdin and clones freely. A single mutex over the whole transport would deadlock the moment the reader blocked on stdin — which it does after each turn. Other notes: - find_cli() mirrors the Python search path (PATH, then ~/.npm-global/bin, /usr/local/bin, ~/.local/bin, ~/node_modules/.bin, ~/.yarn/bin, ~/.claude/local/claude). - build_command() faithfully ports _build_command() with the v0.1 option subset. - Env handling matches Python: filter CLAUDECODE on inherit, set CLAUDE_CODE_ENTRYPOINT=sdk-rust, layer user env, stamp CLAUDE_AGENT_SDK_VERSION last. - Stdout JSON parsing speculatively accumulates until serde_json succeeds or max_buffer_size (1 MiB default) overflows — same buffer-and-retry loop as the Python TextReceiveStream path. Non-JSON chatter ([SandboxDebug] etc.) is skipped between frames. - TransportHandle::close() gives the subprocess a 5s graceful shutdown window after stdin EOF before SIGKILL, mirroring the #625 fix in the Python SDK. - Drop on TransportHandle starts a best-effort kill so abandoned clients do not leak claude processes. Unit tests cover the JSON accumulator (full + partial + complete, non-JSON skip, overflow, multiline split) and the version parser. |
||
|---|---|---|
| src | ||
| .gitignore | ||
| Cargo.toml | ||
| LICENSE | ||
| README.md | ||
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 Rust types. The Python SDK's
two entry points map directly:
query()for fire-and-forget one-shot prompts.ClaudeSDKClient→claude_agent_sdk::Clientfor bidirectional multi-turn sessions.
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 to v0.2. See v0.1 scope below.
Install
[dependencies]
claude-agent-sdk = { git = "https://gitea.sulkta.com/Sulkta-Coop/claude-agent-sdk-rust" }
tokio = { version = "1", features = ["full"] }
tokio-stream = "0.1"
Prerequisites: the claude CLI on PATH (or supply
ClaudeAgentOptions::with_cli_path()), and an authenticated Claude
session. The SDK does not bundle the CLI binary.
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(())
}
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 next Result, then disconnect.
while let Some(msg) = messages.next().await {
if let Message::Result(_) = msg? {
break;
}
}
client.disconnect().await?;
Ok(())
}
See the examples/ directory for runnable variants.
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 { .. } |
v0.1 scope and known limitations
The v0.1 port covers the core path. The following are deferred:
- Control protocol —
interrupt(),set_permission_mode(),set_model(),get_mcp_status(), etc. The Python SDK sends a JSON-RPCinitializerequest before the first user message and handles control requests/responses over the same stdio pair. The RustClientcurrently speaks only the bare user / assistant / result frames. Adding the control protocol unlocks the rest of the API. can_use_toolpermission callback — requires the control protocol.- In-process MCP servers (
@tool/create_sdk_mcp_server) — the Python decorator wraps amcp.server.Serverinstance. The Rust shape for this (likely a derive macro on aTooltrait + a runtime that multiplexes over the control protocol) is not yet drafted. HookMatcher— wire format is supported on the CLI side but the Rust callback surface is not designed.SessionStoremirroring adapter.Sandboxsettings, plugins, agents dataclass.
Each of these degrades gracefully — a v0.1 caller using a newer CLI sees fewer features, not breakage.
License
MIT. See LICENSE.
This is an independent port; the upstream Python SDK is © Anthropic, PBC.