# claude-cli-sdk
[](https://crates.io/crates/claude-cli-sdk)
[](https://docs.rs/claude-cli-sdk)
[](https://github.com/pomdotdev/claude-cli-sdk/actions/workflows/ci.yml)
[](#license)
Rust SDK for programmatic interaction with the [Claude Code CLI](https://www.anthropic.com/claude-code).
> **Disclaimer**: This is an unofficial, community-developed SDK and is not affiliated with, endorsed by, or sponsored by Anthropic, PBC. "Claude" and "Claude Code" are trademarks of Anthropic. This crate interacts with the Claude Code CLI but does not contain any Anthropic proprietary code.
Provides strongly-typed, async-first access to Claude Code sessions via the CLI's NDJSON stdio protocol. Supports one-shot queries, streaming responses, multi-turn sessions, permission callbacks, lifecycle hooks, and MCP server configuration.
**Platform support**: macOS, Linux, and Windows.
## Features
- **`query()`** — one-shot: send a prompt, collect all response messages.
- **`query_stream()`** — streaming: yield messages as they arrive.
- **`Client`** — stateful multi-turn sessions with `send()` and `receive_messages()`.
- **Permission callbacks** — `CanUseToolCallback` for programmatic tool approval.
- **Lifecycle hooks** — `HookMatcher` on `PreToolUse` / `PostToolUse` / `Stop` events.
- **MCP server config** — attach external MCP servers to a session.
- **Message callback** — observe or filter messages before they reach your code.
- **`testing` feature** — `MockTransport`, `ScenarioBuilder`, and message builders for unit tests without a live CLI.
## Quick Start
Add to `Cargo.toml`:
```toml
[dependencies]
claude-cli-sdk = "0.1"
tokio = { version = "1", features = ["macros", "rt-multi-thread"] }
```
### One-shot query
```rust
use claude_cli_sdk::{query, ClientConfig};
#[tokio::main]
async fn main() -> claude_cli_sdk::Result<()> {
let config = ClientConfig::builder()
.prompt("List the files in /tmp")
.build();
let messages = query(config).await?;
for msg in &messages {
if let Some(text) = msg.assistant_text() {
println!("{text}");
}
}
Ok(())
}
```
### Streaming query
```rust
use claude_cli_sdk::{query_stream, ClientConfig};
use tokio_stream::StreamExt;
#[tokio::main]
async fn main() -> claude_cli_sdk::Result<()> {
let config = ClientConfig::builder()
.prompt("Explain ownership in Rust")
.model("claude-opus-4-6")
.build();
let mut stream = query_stream(config).await?;
tokio::pin!(stream);
while let Some(msg) = stream.next().await {
let msg = msg?;
if let Some(text) = msg.assistant_text() {
print!("{text}");
}
}
Ok(())
}
```
### Multi-turn session
```rust
use claude_cli_sdk::{Client, ClientConfig};
use tokio_stream::StreamExt;
#[tokio::main]
async fn main() -> claude_cli_sdk::Result<()> {
let config = ClientConfig::builder()
.prompt("Start a Rust project")
.build();
let mut client = Client::new(config)?;
let session_info = client.connect().await?;
println!("Session: {}", session_info.session_id);
// First turn (prompt sent via config).
let mut stream = client.receive_messages()?;
tokio::pin!(stream);
while let Some(msg) = stream.next().await {
// process...
drop(msg);
break;
}
drop(stream);
// Subsequent turns.
let stream2 = client.send("Now add a test suite")?;
tokio::pin!(stream2);
while let Some(msg) = stream2.next().await {
if let Some(text) = msg?.assistant_text() {
print!("{text}");
}
}
client.close().await
}
```
### Permission callback
```rust
use claude_cli_sdk::{ClientConfig, PermissionMode, PermissionDecision, PermissionContext};
use std::sync::Arc;
let config = ClientConfig::builder()
.prompt("Edit main.rs")
.permission_mode(PermissionMode::Default)
.can_use_tool(Arc::new(|tool_name: &str, _input: &serde_json::Value, _ctx: PermissionContext| {
let tool_name = tool_name.to_owned();
Box::pin(async move {
// Approve only file reads and writes.
if tool_name.starts_with("Read") || tool_name.starts_with("Write") {
PermissionDecision::allow()
} else {
PermissionDecision::deny("Only file tools are allowed")
}
})
}))
.build();
```
## `ClientConfig` Options
| `prompt` | `String` | **Required.** Initial prompt text. |
| `model` | `Option<String>` | Model name, e.g. `"claude-opus-4-6"`. |
| `cwd` | `Option<PathBuf>` | Working directory for the CLI process. |
| `max_turns` | `Option<u32>` | Maximum agentic turns before stopping. |
| `max_budget_usd` | `Option<f64>` | Cost cap for the session. |
| `permission_mode` | `PermissionMode` | `Default`, `AcceptEdits`, `Plan`, or `BypassPermissions`. |
| `can_use_tool` | `Option<CanUseToolCallback>` | Per-tool permission callback. |
| `system_prompt` | `Option<SystemPrompt>` | Text or preset system prompt. |
| `allowed_tools` | `Vec<String>` | Allowlist of tool names. |
| `disallowed_tools` | `Vec<String>` | Blocklist of tool names. |
| `mcp_servers` | `McpServers` | External MCP server definitions. |
| `hooks` | `Vec<HookMatcher>` | Lifecycle hook registrations. |
| `resume` | `Option<String>` | Resume an existing session by ID. |
| `verbose` | `bool` | Enable verbose CLI output. |
| `connect_timeout` | `Option<Duration>` | Deadline for spawn + init (default: 30s). |
| `close_timeout` | `Option<Duration>` | Deadline for graceful shutdown (default: 10s). |
| `read_timeout` | `Option<Duration>` | Per-message recv deadline (default: None). |
| `default_hook_timeout` | `Duration` | Hook callback fallback timeout (default: 30s). |
| `version_check_timeout` | `Option<Duration>` | `--version` check deadline (default: 5s). |
### Timeout configuration
```rust
use std::time::Duration;
use claude_cli_sdk::ClientConfig;
let config = ClientConfig::builder()
.prompt("Analyze this project")
.connect_timeout(Some(Duration::from_secs(60))) // Wait up to 60s for init
.read_timeout(Some(Duration::from_secs(300))) // Detect hung processes after 5min
.close_timeout(Some(Duration::from_secs(5))) // Kill after 5s on close
.default_hook_timeout(Duration::from_secs(10)) // Hook callbacks get 10s
.build();
```
Set any `Option<Duration>` timeout to `None` to wait indefinitely.
### Lifecycle hooks
Register callbacks for tool execution events:
```rust
use claude_cli_sdk::{ClientConfig, HookMatcher, HookEvent, HookOutput, HookDecision};
use std::sync::Arc;
let config = ClientConfig::builder()
.prompt("Refactor auth module")
.hooks(vec![
HookMatcher::new(HookEvent::PreToolUse, Arc::new(|input, _session, _ctx| {
Box::pin(async move {
eprintln!("Tool: {:?}", input.tool_name);
HookOutput::allow()
})
})).for_tool("Bash"),
])
.build();
```
### MCP server configuration
Attach external MCP servers to a session:
```rust
use claude_cli_sdk::{ClientConfig, McpServerConfig, McpServers};
let mut servers = McpServers::new();
servers.insert(
"filesystem".into(),
McpServerConfig::new("npx")
.with_args(["-y", "@modelcontextprotocol/server-filesystem", "/tmp"]),
);
let config = ClientConfig::builder()
.prompt("List files using the filesystem MCP server")
.mcp_servers(servers)
.build();
```
### Testing with `MockTransport`
Enable the `testing` feature for unit tests without a live CLI:
```toml
[dev-dependencies]
claude-cli-sdk = { version = "0.1", features = ["testing"] }
```
```rust
use std::sync::Arc;
use claude_cli_sdk::Client;
use claude_cli_sdk::config::ClientConfig;
use claude_cli_sdk::testing::{ScenarioBuilder, assistant_text};
let transport = ScenarioBuilder::new("test-session")
.exchange(vec![assistant_text("Hello!")])
.build();
let transport = Arc::new(transport);
let mut client = Client::with_transport(
ClientConfig::builder().prompt("test").build(),
transport,
).unwrap();
```
## Feature Flags
| `testing` | `MockTransport`, `ScenarioBuilder`, and message builder helpers for unit tests |
| `efficiency` | Reserved for future throughput optimizations |
| `integration` | Integration test helpers (requires a live CLI) |
## License
Licensed under either of [Apache License, Version 2.0](LICENSE-APACHE) or [MIT License](LICENSE-MIT) at your option.