Skip to content
talaagentPublic

About

A Rust SDK for Claude Code: the stream-json protocol, typed

Topics

Resources

Contributing

Security policy

Stars

0 stars

Watchers

0 watching

Forks

Repository files navigation

ccwire

CI crates.io docs.rs Rust 1.85+ License: Apache-2.0

A Rust SDK for Claude Code: it drives the claude CLI over its stream-json protocol, the way the official TypeScript and Python Agent SDKs do.

Status: early. This release has the protocol layer, a session's command line (Options), the line framing and the session core, checked against Claude Code 2.1.296 (ccwire::TESTED_CLI_VERSION). Spawning and reaping the claude process, query() and permission/hook/MCP helpers come next.

Crate Purpose Package API docs
ccwire Protocol types, CLI options, framing, and async sessions crates.io rustdoc
ccwire-oauth Claude account usage, profile, and OAuth token refresh crates.io rustdoc

Rust 1.85 or newer is required. This project is not affiliated with or endorsed by Anthropic. Running a live session requires an installed, authenticated claude CLI; parsing saved transcripts does not.

Install

cargo add ccwire                     # the protocol types and sync framing
cargo add ccwire --features runtime  # plus the async framing and the session core

The workspace also has ccwire-oauth, a typed client for the Claude account endpoints an OAuth login can call.

ccwire::protocol

The stream-json protocol as Rust types: pure data and serde, with no I/O and no async runtime.

  • Inbound: every frame the CLI writes (user, assistant, system, result, stream_event, rate_limit_event, control_request, control_response, control_cancel_request).
  • Message: the conversation part of it; Control: the control channel. Inbound::split gives one or the other.
  • UserInput: a user message to write to the CLI's stdin.
  • ControlRequest, ControlResponse, PermissionResult: the control channel, both directions.
  • Partial, PartialMessage: stream_event deltas (--include-partial-messages), read and assembled into a message's text.

It is built to survive a CLI that ships weekly:

  • Parsing a JSON object never fails. An unknown type is Other with the raw JSON; a known type that no longer parses is Malformed with the error and the raw JSON, so a protocol change shows up instead of disappearing. protocol::stats::counts() counts both (and, with the tracing feature, each is logged with its type).
  • Every struct keeps the keys it doesn't model (extra), and optional keys tell absent from null (Field), so a frame serializes back to the same JSON value.
  • Public enums are #[non_exhaustive].
  • A result frame ends a turn, so it parses leniently, and Inbound::ends_turn also matches a result that came out Malformed.
use ccwire::protocol::{Frame, Inbound, Message};

fn on_line(line: &str) {
    let Ok(frame) = Inbound::from_line(line) else { return };
    match frame.split() {
        Frame::Message(Message::Assistant(a)) => {
            for block in a.content() {
                if let Some(text) = block.as_text() {
                    println!("{text}");
                }
            }
        }
        Frame::Message(Message::Result(r)) => println!("cost: {:?}", r.cost_usd()),
        Frame::Message(m) if m.ends_turn() => println!("turn over (unparsed result)"),
        _ => {}
    }
}

ccwire::framing

One JSON frame per line. LineReader (sync) and AsyncLineReader split a byte stream into lines under a maximum length (64 MiB by default): a longer line is skipped and reported, and reading goes on. FrameReader turns lines into Inbound frames and never fails on content: garbage, a cut-off last line or an oversized line comes out as Inbound::Malformed (a result among them still ends the turn). Every line carries the byte offset past it, so a reader can resume a file where it stopped. FrameWriter writes frames.

ccwire::session (feature runtime)

A session over any AsyncRead (the CLI's output) and AsyncWrite (its input), not tied to a child process:

Enable runtime and use a Tokio runtime. The caller starts and reaps the child process; pass its stdout and stdin to Session::new:

use ccwire::{protocol::{Frame, Message}, session::Session};
use tokio::io::{AsyncRead, AsyncWrite};

async fn read_turn<R, W>(child_stdout: R, child_stdin: W) -> Result<(), ccwire::session::Error>
where
    R: AsyncRead + Unpin + Send + 'static,
    W: AsyncWrite + Unpin + Send + 'static,
{
    let mut session = Session::new(child_stdout, child_stdin);
    session.send("hi").await?;
    while let Some(frame) = session.next().await {
        let frame = frame?;
        let ends_turn = frame.ends_turn();
        if let Frame::Message(Message::Assistant(message)) = frame.split() {
            for block in message.content() {
                if let Some(text) = block.as_text() {
                    println!("{text}");
                }
            }
        }
        if ends_turn {
            break;
        }
    }
    session.close().await?;
    Ok(())
}

For a complete executable that starts the CLI, sends a prompt, reads the response, and waits for the child, see examples/chat.rs:

cargo run -p ccwire --example chat --features runtime -- "Say hello in one sentence."

The example disables tools and limits the turn count. A live run uses your Claude account. For offline transcript parsing:

cargo run -p ccwire --example transcript -- path/to/transcript.jsonl
  • Control round trips: request (with interrupt, set_model, set_permission_mode, stop_task and request_raw on top) gets a fresh request_id and resolves on the matching control_response.
  • The CLI's own control requests (can_use_tool, ...) go to a ControlHandler, each on its own task, or come out of the stream to answer with respond. A control_cancel_request aborts the handler.
  • SessionState (Idle, InTurn, Closed), watchable.
  • The run-end rule: finish() closes the input at once without callbacks, and once idle with them.
  • One writer task, so dropping a send or request future never leaves half a line; dropping the Session aborts its tasks.

Features

  • runtime (off by default): the async framing and the session core, on tokio (io-util, sync, rt).
  • tracing (off by default): log each Other (debug) and Malformed (warn) frame with its type.

Dependencies

serde, serde_json and thiserror; tokio with runtime; tracing with tracing.

Changelogs

See ccwire changes and ccwire-oauth changes. Each crate is versioned separately.

Contributing

See CONTRIBUTING.md for local checks, the workspace layout, protocol fixtures, and the release process. Report bugs and request features in GitHub Issues. For vulnerabilities, use the private reporting instructions in SECURITY.md.

License

Licensed under the Apache License, Version 2.0.

About

A Rust SDK for Claude Code: the stream-json protocol, typed

Topics

Resources

Contributing

Security policy

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages