GitHub Copilot CLI SDK for Rust
A Rust SDK for programmatic access to the GitHub Copilot CLI.
Note: This SDK is in technical preview and may change in breaking ways.
See github/copilot-sdk for the equivalent SDKs in TypeScript, Python, Go, and .NET. The Rust SDK seeks parity with those SDKs; see Differences From Other SDKs below for the small set of intentional divergences.
Quick Start
use Arc;
use ;
use ApproveAllHandler;
# async
Architecture
Your Application
↓
github_copilot_sdk::Client (manages CLI process lifecycle)
↓
github_copilot_sdk::Session (per-session event loop + handler dispatch)
↓ JSON-RPC over stdio or TCP
copilot --server --stdio
The SDK manages the CLI process lifecycle: spawning, health-checking, and graceful shutdown. Communication uses JSON-RPC 2.0 over stdin/stdout with Content-Length framing (the same protocol used by LSP). TCP transport is also supported.
API Reference
Client
// Start a client (spawns CLI process)
let client = start.await?;
// Create a new session
let session = client.create_session.await?;
// Resume an existing session
let session = client.resume_session.await?;
// Low-level RPC
let result = client.call.await?;
let response = client.send_request.await?;
// Health check (echoes message back, returns typed PingResponse)
let pong = client.ping.await?;
// Shutdown
client.stop.await?;
ClientOptions:
| Field | Type | Description |
|---|---|---|
program |
CliProgram |
Resolve (default: auto-detect) or Path(PathBuf) (explicit) |
prefix_args |
Vec<OsString> |
Args before --server (e.g. script path for node) |
cwd |
PathBuf |
Working directory for CLI process |
env |
Vec<(OsString, OsString)> |
Environment variables for CLI process |
env_remove |
Vec<OsString> |
Environment variables to remove |
extra_args |
Vec<String> |
Extra CLI flags |
transport |
Transport |
Stdio (default), Tcp { port }, or External { host, port } |
With the default CliProgram::Resolve, Client::start() automatically resolves the binary via github_copilot_sdk::resolve::copilot_binary() — checking COPILOT_CLI_PATH, the embedded CLI, and then the system PATH. Use CliProgram::Path(path) to skip resolution.
Session
Created via Client::create_session or Client::resume_session. Owns an internal event loop that dispatches events to the SessionHandler.
use MessageOptions;
// Simple send — &str / String convert into MessageOptions automatically.
// Returns the assigned message ID for correlation with later events.
let _id = session.send.await?;
// Send with mode and attachments
let _id = session
.send
.await?;
// Message history
let messages = session.get_messages.await?;
// Abort the current agent turn
session.abort.await?;
// Model management
let model = session.get_model.await?;
session.set_model.await?;
// Mode management (interactive, plan, autopilot)
let mode = session.get_mode.await?;
session.set_mode.await?;
// Workspace files
let files = session.list_workspace_files.await?;
let content = session.read_workspace_file.await?;
// Plan management
let = session.read_plan.await?;
session.update_plan.await?;
// Fleet (sub-agents)
session.start_fleet.await?;
// Cleanup (preserves on-disk session state for later resume)
session.disconnect.await?;
Typed RPC namespace
The ergonomic helpers above are convenience wrappers over a fully-typed
JSON-RPC namespace generated from the GitHub Copilot CLI schema. Client::rpc()
and Session::rpc() give direct access to every method on the wire,
including ones with no helper today, with strongly-typed request and
response structs.
// Methods with helpers — wire strings live in one generated place.
let files = session.rpc.workspaces.list_files.await?.files;
let models = client.rpc.models.list.await?.models;
// Methods with no helper — full schema-typed access.
let agents = session.rpc.agent.list.await?.agents;
let tasks = session.rpc.tasks.list.await?.tasks;
let forked = client
.rpc
.sessions
.fork
.await?;
New RPCs land in the namespace immediately as the schema regenerates; helpers are added on top only when an ergonomic story is worth the maintenance.
SessionHandler
Implement this trait to control how a session responds to CLI events. Two styles are supported:
1. Per-event methods (recommended). Override only the callbacks you care about; every method has a safe default (permission → deny, user input → none, external tool → "no handler", elicitation → cancel, exit plan → default). This is the serenity::EventHandler pattern.
use async_trait;
use ;
use ;
;
2. Single on_event method. Override on_event directly and match on HandlerEvent — useful for logging middleware, custom routing, or when you want one exhaustive dispatch point.
use *;
use async_trait;
The default on_event dispatches to the per-event methods, so overriding on_event short-circuits them entirely — pick one style per handler.
Events are processed serially per session — blocking in a handler method pauses that session's event loop (which is correct, since the CLI is also waiting for the response). Other sessions are unaffected.
Note: Notification-triggered events (
PermissionRequestviapermission.requested,ExternalToolviaexternal_tool.requested) are dispatched on spawned tasks and may run concurrently with the serial event loop. See the trait-level docs onSessionHandlerfor details.
SessionConfig
let config = SessionConfig ;
let session = client.create_session.await?;
Session Hooks
Hooks intercept CLI behavior at lifecycle points — tool use, prompt submission, session start/end, and errors. Install a SessionHooks impl with [SessionConfig::with_hooks] — the SDK auto-enables hooks in SessionConfig when one is set.
use Arc;
use *;
use async_trait;
;
let session = client
.create_session
.await?;
Hook events: PreToolUse, PostToolUse, UserPromptSubmitted, SessionStart, SessionEnd, ErrorOccurred. Each carries typed input/output structs. Return HookOutput::None for events you don't handle.
System Message Transforms
Transforms customize system message sections during session creation. The SDK injects action: "transform" entries for each section ID your transform handles.
use *;
use async_trait;
;
let session = client
.create_session
.await?;
Tool Registration
Define client-side tools as named types with ToolHandler, then route them with ToolHandlerRouter. Enable the derive feature for schema_for::<T>() — it generates JSON Schema from Rust types via schemars.
use Arc;
use ApproveAllHandler;
use ;
use ;
use Deserialize;
use async_trait;
;
// Build a router that dispatches tool calls by name
let router = new;
let config = SessionConfig
.with_handler;
let session = client.create_session.await?;
Tools are named types (not closures) — visible in stack traces and navigable via "go to definition". The router implements SessionHandler, forwarding unrecognized tools and non-tool events to the inner handler.
For trivial tools that don't need a named type, define_tool collapses the definition to a single expression:
use ;
use ToolResult;
use Deserialize;
let router = new;
The closure receives the full ToolInvocation alongside the deserialized parameters, so handlers that need inv.session_id or inv.tool_call_id for telemetry, streaming updates, or scoped lookups can use them directly. Use _inv when you don't need the metadata.
Reach for the ToolHandler trait directly when you need shared state across multiple methods or want a named type that shows up by name in stack traces.
Permission Policies
Set a permission policy directly on SessionConfig with the chainable builders. They wrap whatever handler you've installed (defaulting to DenyAllHandler if none) so only permission requests are intercepted; every other event flows through unchanged.
let session = client
.create_session
.await?;
Call the policy method after
with_handler—with_handleroverwrites the handler field, soapprove_all_permissions().with_handler(...)discards the wrap.
For composing a policy onto a handler outside the builder chain (e.g. when wrapping a ToolHandlerRouter you've built elsewhere), the permission module exposes the same primitives as free functions:
use permission;
let router = new;
let handler = approve_all;
// or permission::deny_all(...) / permission::approve_if(..., predicate)
let session = client.create_session.await?;
Capabilities & Elicitation
The SDK negotiates capabilities with the CLI after session creation. Enable elicitation to let the agent present structured UI dialogs (forms, URL prompts) to the user.
let config = SessionConfig ;
The handler receives HandlerEvent::ElicitationRequest with a message, optional JSON Schema for form fields, and an optional mode. Known modes include Form and Url, but the mode may be absent or an unknown future value. Return HandlerResponse::Elicitation(result).
User Input Requests
Some sessions ask the user free-form questions (or multiple-choice prompts) outside the elicitation flow. Implement SessionHandler::on_user_input and the SDK will forward userInput.request callbacks:
async
Return None to signal "no answer available" (the CLI falls back to its own prompt). Enable via SessionConfig::request_user_input (defaults to Some(true)).
Slash Commands
Register named commands so users can invoke them as /name args from the TUI:
use ;
use async_trait;
;
let mut config = default;
config.commands = Some;
Only name and description are sent over the wire; the handler stays in your process. Returning Err(_) surfaces the message back through the TUI.
Streaming
Set streaming: true to receive incremental delta events alongside finalized messages:
let mut config = default;
config.streaming = Some;
let mut events = session.subscribe;
while let Ok = events.recv.await
When streaming is off (the default), only the final assistant.message and assistant.reasoning events fire. Delta events arrive in order; concatenating their delta text payloads reproduces the final message.
Infinite Sessions
Enable the SDK's session-store integration so conversations persist across CLI restarts and grow beyond the model's context window via automatic compaction:
use InfiniteSessionConfig;
let mut infinite = default;
infinite.workspace_path = Some;
let mut config = default;
config.infinite_sessions = Some;
The CLI emits session.compaction_start / session.compaction_complete events around each compaction. The session id remains stable across compactions; resume with Client::resume_session to pick up a prior conversation. Workspace state lives under ~/.copilot/session-state/{sessionId} by default — override with workspace_path to relocate.
Custom Providers (BYOK)
Route model traffic through your own inference endpoint instead of GitHub's hosted models:
use ProviderConfig;
let mut provider = default;
provider.provider_type = Some;
provider.base_url = "https://my-proxy.example.com/v1".to_string;
provider.bearer_token = Some;
let mut config = default;
config.provider = Some;
Provider types include "openai", "azure", and "anthropic". Set wire_api to "completions" or "responses" (OpenAI/Azure only). Custom headers go in provider.headers. The SDK forwards the configuration to the CLI verbatim — the CLI handles the upstream call, including authentication.
Telemetry
Forward OpenTelemetry signals from the spawned CLI process to your collector:
use ;
let mut telem = default;
telem.exporter_type = Some;
telem.otlp_endpoint = Some;
telem.source_name = Some;
let mut opts = default;
opts.telemetry = Some;
let client = start.await?;
The SDK injects the appropriate environment variables (COPILOT_OTEL_EXPORTER_TYPE, OTEL_EXPORTER_OTLP_ENDPOINT, ...) into the spawned CLI process. The SDK takes no OpenTelemetry dependency; the CLI itself owns the exporter pipeline. Caller-supplied ClientOptions::env entries override telemetry-injected values.
Progress Reporting (send_and_wait)
For fire-and-forget messaging where you need to block until the agent finishes:
use Duration;
use MessageOptions;
// Sends a message and blocks until session.idle or session.error
session
.send_and_wait
.await?;
Default timeout is 60 seconds. Only one send_and_wait can be active per session — concurrent calls return an error.
Newtypes
SessionId — a newtype wrapper around String that prevents accidentally passing workspace IDs or request IDs where session IDs are expected. Transparent serialization (#[serde(transparent)]), zero-cost Deref<Target=str>, and ergonomic comparisons with &str and String.
use SessionId;
let id = new;
assert_eq!; // compare with &str
let raw: String = id.into_inner; // unwrap when needed
Error Handling
The SDK uses a typed error enum:
// Check if the transport is broken (caller should discard the client)
if err.is_transport_failure
Differences From Other SDKs
The Rust SDK aligns closely with the Node, Python, and Go SDKs but diverges in a few places where Rust idiom or the type system gives a clearly better shape, and exposes a small additional surface where the language affords ergonomics the dynamically-typed SDKs don't.
Shape divergence
SessionFsProviderregistration is direct, not factory-closure. Where Node/Python/Go accept a closure that the runtime calls on each session-create to build a fresh provider, the Rust SDK takesArc<dyn SessionFsProvider>directly via [SessionConfig::with_session_fs_provider]. The factory pattern doesn't cleanly express in Rust at the session-config call site — there is noSessionvalue to thread in, and the SDK already prefers traits over boxed closures for handler-shaped APIs (SessionHandler,SessionHooks,ToolHandler).
use Arc;
use ;
let mut options = default;
options.session_fs = Some;
let client = start.await?;
let session = client
.create_session
.await?;
See examples/session_fs.rs for a complete
in-memory provider implementation.
Rust-only API
A handful of conveniences exist only on the Rust SDK as of 0.1.0. These are surface areas where Rust idiom (newtypes, enums, trait objects) gives a clearly nicer shape than Node/Python/Go currently expose. Rust gets to be Rust here — cross-SDK parity for these is a post-release conversation, not a release blocker. None of these are deprecated and none of them are scheduled for removal.
- Typed newtypes —
SessionIdandRequestIdare#[serde(transparent)]newtypes aroundString, so the type system distinguishes a session identifier from an arbitraryStringat compile time. Node/Python/Go use bare strings. - Permission policy builders —
permission::approve_all,permission::deny_all, andpermission::approve_if(handler, predicate)incrate::permissionprovide composable, no-handler-needed permission shortcuts that wrap an existingSessionHandler. Other SDKs require a full handler implementation for these patterns. Client::from_streams— connect to a CLI server over arbitrary caller-suppliedAsyncRead/AsyncWrite. Useful for testing, in-process embedding, or custom transports. Other SDKs are spawn-only or fixed-stdio.enum Transport { Stdio, Tcp, External }— explicit, exhaustive transport selector onClientOptions::transport. Node/Python/Go rely on conditional config field combinations instead.- Split
prefix_args/extra_argsonClientOptions— separate arg vectors for "prepend before subcommand" vs "append after the built-in flags", giving precise control over CLI invocation order without string-splicing. SessionHandler::on_auto_mode_switch— typed handler for the CLI's rate-limit-recovery prompt (CLI'sautoModeSwitch.requestcallback, added in copilot-agent-runtime PR #7024). ReturnsAutoModeSwitchResponse::{Yes, YesAlways, No}. Default impl declines. Cross-SDK parity is post-release follow-up — Node / Python / Go / .NET consumers currently observe the request as a raw event and must drive the wire response themselves.
Layout
| File | Description |
|---|---|
lib.rs |
Client, ClientOptions, CliProgram, Transport, Error |
session.rs |
Session struct, event loop, send/send_and_wait, Client::create_session/resume_session |
subscription.rs |
EventSubscription / LifecycleSubscription (Stream-able observer handles for subscribe() / subscribe_lifecycle()) |
handler.rs |
SessionHandler trait, HandlerEvent/HandlerResponse enums, ApproveAllHandler |
hooks.rs |
SessionHooks trait, HookEvent/HookOutput enums, typed hook inputs/outputs |
transforms.rs |
SystemMessageTransform trait, section-level system message customization |
tool.rs |
ToolHandler trait, ToolHandlerRouter, schema_for::<T>() (with derive feature) |
types.rs |
CLI protocol types (SessionId, SessionEvent, SessionConfig, Tool, etc.) |
resolve.rs |
Binary resolution (copilot_binary, node_binary, extended_path) |
embeddedcli.rs |
Embedded CLI extraction (embedded-cli feature) |
router.rs |
Internal per-session event demux |
jsonrpc.rs |
Internal Content-Length framed JSON-RPC transport |
Embedded CLI
By default, copilot_binary() searches COPILOT_CLI_PATH, the system PATH, and common install locations. To ship with a specific CLI version embedded in the binary, set COPILOT_CLI_VERSION at build time:
COPILOT_CLI_VERSION=1.0.15
How it works
-
Build time: The SDK's
build.rsdetectsCOPILOT_CLI_VERSION, downloads the platform-appropriate archive from thegithub/copilot-cliGitHub Releases (copilot-{platform}.tar.gzon macOS/Linux,.zipon Windows), verifies the archive's SHA-256 against the release'sSHA256SUMS.txt, extracts thecopilotbinary, compresses it with zstd, and embeds viainclude_bytes!(). No extra steps or tools needed — just the env var. -
Runtime: On the first call to
github_copilot_sdk::resolve::copilot_binary(), the embedded binary is lazily extracted to~/.cache/github-copilot-sdk-{version}/copilot(orcopilot.exeon Windows), SHA-256 verified, and cached. Subsequent calls return the cached path. -
Dev builds: Without the env var,
build.rsdoes nothing. The binary is resolved from PATH as usual — zero friction.
Resolution priority
copilot_binary() checks these sources in order:
COPILOT_CLI_PATHenvironment variable- Embedded CLI (build-time, via
COPILOT_CLI_VERSION) - System PATH + common install locations
Platforms
Supported: darwin-arm64, darwin-x64, linux-x64, linux-arm64, win32-x64, win32-arm64. The target platform is auto-detected from CARGO_CFG_TARGET_OS and CARGO_CFG_TARGET_ARCH (cross-compilation works).
Features
No features are enabled by default — the bare SDK resolves the CLI from COPILOT_CLI_PATH or the system PATH without pulling in additional feature-gated dependencies.
| Feature | Default | Description |
|---|---|---|
embedded-cli |
— | Build-time CLI embedding via COPILOT_CLI_VERSION (adds sha2, zstd). Enable when you need to ship a self-contained binary with a pinned CLI version. |
derive |
— | schema_for::<T>() for generating JSON Schema from Rust types (adds schemars). Enable when defining tool parameters. |
# These examples use registry syntax for illustration; until the crate is
# published, use a path or git dependency instead.
# Minimal — resolve CLI from PATH
= "0.1"
# Ship a pinned CLI version in your binary
= { = "0.1", = ["embedded-cli"] }
# Derive JSON Schema for tool parameters
= { = "0.1", = ["derive"] }
# Both
= { = "0.1", = ["embedded-cli", "derive"] }