62 KiB
Claude Code — Rust Codebase
Overview
The Rust codebase at claude-code-rust/ is a complete standalone rewrite of the TypeScript Claude Code CLI in async Rust. It is not an FFI binding layer, not a partial port, and shares no runtime code with the TypeScript implementation. It re-implements the same tool names and semantics, permission model, CLAUDE.md discovery, auto-compact logic, MCP (Model Context Protocol) client, bridge protocol, and cron scheduler — all in async Rust using the Tokio runtime.
Architecture
claude-code-rust/
├── Cargo.toml # Workspace root
└── crates/
├── core/ (cc-core) # Shared types, config, permissions, history, hooks
├── api/ (cc-api) # API client + SSE streaming
├── tools/ (cc-tools) # All tool implementations (33 tools)
├── query/ (cc-query) # Agentic query loop, compact, cron scheduler
├── tui/ (cc-tui) # ratatui terminal UI
├── commands/ (cc-commands) # Slash command implementations
├── mcp/ (cc-mcp) # MCP (Model Context Protocol) client
├── bridge/ (cc-bridge) # Bridge to claude.ai web UI
└── cli/ (claude-code) # Binary entry point
Dependency flow:
cli → query → tools → core
↓ ↗
api → core
↓
commands → core
↓
tui → core
↓
mcp → core
↓
bridge → core
Workspace Root: Cargo.toml
Path: claude-code-rust/Cargo.toml
Cargo workspace with resolver = "2", edition 2021, version 1.0.0 across all member crates.
Workspace Members
| Member Path | Package Name | Type |
|---|---|---|
crates/core |
cc-core |
Library |
crates/api |
cc-api |
Library |
crates/tools |
cc-tools |
Library |
crates/query |
cc-query |
Library |
crates/tui |
cc-tui |
Library |
crates/commands |
cc-commands |
Library |
crates/mcp |
cc-mcp |
Library |
crates/bridge |
cc-bridge |
Library |
crates/cli |
claude-code |
Binary ([[bin]] name = "claude") |
Key Shared Dependencies
| Crate | Version | Features |
|---|---|---|
tokio |
1.44 | full |
reqwest |
0.12 | json, stream, rustls-tls |
ratatui |
0.29 | default |
crossterm |
0.28 | event-stream |
clap |
4 | derive, env, string |
serde |
1 | derive |
serde_json |
1 | default |
anyhow |
1 | default |
thiserror |
2 | default |
tracing |
0.1 | default |
tracing-subscriber |
0.3 | env-filter |
uuid |
1 | v4 |
chrono |
0.4 | serde |
regex |
1 | default |
glob |
0.3 | default |
walkdir |
2 | default |
similar |
2 | default (declared, not heavily used) |
once_cell |
1 | default |
parking_lot |
0.12 | default |
dashmap |
6 | default |
tokio-util |
0.7 | codec, sync |
async-trait |
0.1 | default |
schemars |
0.8 | derive |
nix |
0.29 | process, signal, user |
base64 |
0.22 | default |
sha2 |
0.10 | default |
hex |
0.4 | default |
Crate: cc-core
Path: crates/core/src/lib.rs
Central shared crate. Defines all types consumed by every other crate. Contains 9 inline submodules.
Module: error
ClaudeError enum (implements std::error::Error via thiserror):
Api(String)— Generic API errorApiStatus { status_code: u16, message: String }— HTTP status errorAuth(String)— Authentication failurePermissionDenied(String)— Tool permission deniedTool(String)— Tool execution errorIo(#[from] std::io::Error)— I/O errorJson(#[from] serde_json::Error)— JSON parse errorHttp(#[from] reqwest::Error)— HTTP client errorRateLimit { retry_after: Option<u64> }— 429 rate limitContextWindowExceeded— Context window fullMaxTokensReached— max_tokens hitCancelled— User/signal cancellationConfig(String)— Config load/save errorMcp(String)— MCP protocol errorOther(String)— Catch-all
Key methods:
is_retryable(&self) -> bool— true forRateLimitandApiStatuswith code 529is_context_limit(&self) -> bool— true forContextWindowExceededandMaxTokensReached
Module: types
Role enum: User, Assistant
ContentBlock enum (serde untagged):
Text { text: String }Image { source: ImageSource }ToolUse { id: String, name: String, input: Value }ToolResult { tool_use_id: String, content: ToolResultContent, is_error: Option<bool> }Thinking { thinking: String, signature: String }RedactedThinking { data: String }Document { source: DocumentSource, citations: Option<CitationsConfig> }
MessageContent enum (serde untagged):
Text(String)Blocks(Vec<ContentBlock>)
Message struct:
- Fields:
role: Role,content: MessageContent Message::user(text)— convenience constructorMessage::assistant(text)— convenience constructorMessage::user_blocks(blocks: Vec<ContentBlock>)— multi-block user messageMessage::assistant_blocks(blocks)— multi-block assistant messageget_text() -> Option<&str>— first Text block or Text contentget_all_text() -> String— concatenate all text blocksget_tool_use_blocks() -> Vec<ContentBlock>— filter ToolUse blocksget_thinking_blocks() -> Vec<ContentBlock>— filter Thinking blockshas_tool_use() -> boolcontent_blocks() -> &[ContentBlock]
UsageInfo struct:
- Fields:
input_tokens: u32,output_tokens: u32,cache_creation_input_tokens: u32,cache_read_input_tokens: u32 total_input() -> u32— sum of input + cache tokenstotal() -> u32— sum of all tokens- Implements
Default
ToolDefinition struct: { name: String, description: String, input_schema: Value }
Supporting types: MessageCost, ImageSource { type, media_type, data }, DocumentSource, CitationsConfig, ToolResultContent (Text/Blocks enum)
Module: config
Config struct — runtime configuration:
api_key: Option<String>api_base: Option<String>model: Stringmax_tokens: u32permission_mode: PermissionModeverbose: booloutput_format: OutputFormatmax_turns: u32system_prompt: Option<String>append_system_prompt: Option<String>no_claude_md: boolauto_compact: boolthinking_budget: Option<u32>mcp_servers: Vec<McpServerConfig>hooks: HashMap<HookEvent, Vec<HookEntry>>
Key methods:
resolve_api_key() -> Option<String>— checksconfig.api_keythenANTHROPIC_API_KEYenv varresolve_api_base() -> String— checksANTHROPIC_BASE_URLenv var, falls back to constanteffective_model() -> &streffective_max_tokens() -> u32
PermissionMode enum:
Default— allow read-only operations automaticallyAcceptEdits— allow all edits automaticallyBypassPermissions— allow everything without promptingPlan— read-only planning mode
OutputFormat enum: Text, Json, StreamJson
HookEvent enum: PreToolUse, PostToolUse, Stop, UserPromptSubmit, Notification
HookEntry struct: { command: String, tool_filter: Option<String>, blocking: bool }
McpServerConfig struct: { name: String, command: String, args: Vec<String>, env: HashMap<String, String>, url: Option<String>, server_type: McpServerType }
Settings struct — persisted user preferences at ~/.claude/settings.json:
async fn load() -> Result<Settings>— deserializes JSON, returns default on missing fileasync fn save(&self) -> Result<()>— serializes to JSON, creates parent dirs
Module: constants
All constants are pub const:
| Constant | Value |
|---|---|
APP_NAME |
"claude" |
DEFAULT_MODEL |
"claude-opus-4-6" |
SONNET_MODEL |
"claude-sonnet-4-6" |
HAIKU_MODEL |
"claude-haiku-4-5-20251001" |
DEFAULT_MAX_TOKENS |
32_000 |
MAX_TOKENS_HARD_LIMIT |
65_536 |
DEFAULT_COMPACT_THRESHOLD |
0.9 |
MAX_TURNS_DEFAULT |
10 |
ANTHROPIC_API_BASE |
"https://api.anthropic.com" |
ANTHROPIC_API_VERSION |
"2023-06-01" |
ANTHROPIC_BETA_HEADER |
"interleaved-thinking-2025-05-14,token-efficient-tools-2025-02-19,files-api-2025-04-14" |
CLAUDE_MD_FILENAME |
"CLAUDE.md" |
SETTINGS_FILENAME |
"settings.json" |
HISTORY_FILENAME |
"history.json" |
CONFIG_DIR_NAME |
".claude" |
Tool name constants:
TOOL_NAME_BASH = "Bash"TOOL_NAME_FILE_EDIT = "Edit"TOOL_NAME_FILE_READ = "Read"TOOL_NAME_FILE_WRITE = "Write"TOOL_NAME_GLOB = "Glob"TOOL_NAME_GREP = "Grep"TOOL_NAME_WEB_FETCH = "WebFetch"TOOL_NAME_WEB_SEARCH = "WebSearch"TOOL_NAME_NOTEBOOK_EDIT = "NotebookEdit"TOOL_NAME_AGENT = "Task"(sub-agent)TOOL_NAME_TODO_WRITE = "TodoWrite"TOOL_NAME_ASK_USER = "AskUserQuestion"TOOL_NAME_ENTER_PLAN_MODE = "EnterPlanMode"TOOL_NAME_EXIT_PLAN_MODE = "ExitPlanMode"TOOL_NAME_POWERSHELL = "PowerShell"TOOL_NAME_SLEEP = "Sleep"TOOL_NAME_CRON_CREATE = "CronCreate"TOOL_NAME_CRON_DELETE = "CronDelete"TOOL_NAME_CRON_LIST = "CronList"TOOL_NAME_ENTER_WORKTREE = "EnterWorktree"TOOL_NAME_EXIT_WORKTREE = "ExitWorktree"TOOL_NAME_LIST_MCP_RESOURCES = "ListMcpResources"TOOL_NAME_READ_MCP_RESOURCE = "ReadMcpResource"TOOL_NAME_TOOL_SEARCH = "ToolSearch"TOOL_NAME_BRIEF = "Brief"TOOL_NAME_CONFIG = "Config"TOOL_NAME_SEND_MESSAGE = "SendMessage"TOOL_NAME_SKILL = "Skill"
Module: context
ContextBuilder — builds system context strings injected into the system prompt:
-
build_system_context(working_dir: &Path) -> String- Platform (OS + architecture)
- Current working directory
- Git status (runs
git status --short) - Last 5 git commits (runs
git log --oneline -5)
-
build_user_context(working_dir: &Path, no_claude_md: bool) -> String- Current date/time (from
chrono::Local::now()) - CLAUDE.md discovery: walks from
working_dirup to filesystem root, collecting anyCLAUDE.mdfiles; also reads~/.claude/CLAUDE.md - Returns concatenated content of all discovered CLAUDE.md files
- Current date/time (from
Module: permissions
PermissionDecision enum: Allow, AllowPermanently, Deny, DenyPermanently
PermissionRequest struct: { tool_name: String, description: String, details: Option<String>, is_read_only: bool }
PermissionHandler trait:
check_permission(&self, tool_name: &str) -> PermissionDecisionrequest_permission(&self, request: &PermissionRequest) -> PermissionDecision
AutoPermissionHandler — automatic non-interactive handler:
BypassPermissions→Allowall requestsAcceptEdits→Allowall requestsPlan→Allowonly ifis_read_only == true, elseDenyDefault→Allowonly ifis_read_only == true, elseDeny
Module: history
ConversationSession struct:
id: String (UUID v4)
created_at: DateTime<Utc>
updated_at: DateTime<Utc>
messages: Vec<Message>
model: String
title: Option<String>
working_dir: String
Functions:
save_session(session: &ConversationSession) -> Result<()>— writes to~/.claude/conversations/<id>.jsonload_session(id: &str) -> Result<Option<ConversationSession>>— reads from path abovelist_sessions() -> Result<Vec<ConversationSession>>— reads all.jsonfiles in~/.claude/conversations/, sorts byupdated_atdescendingdelete_session(id: &str) -> Result<()>— removes the file
Module: cost
ModelPricing struct: { input_per_mtok: f64, output_per_mtok: f64, cache_creation_per_mtok: f64, cache_read_per_mtok: f64 }
Pricing constants:
| Model | Input ($/MTok) | Output ($/MTok) |
|---|---|---|
OPUS |
$15.00 | $75.00 |
SONNET |
$3.00 | $15.00 |
HAIKU |
$0.80 | $4.00 |
CostTracker struct — lock-free using AtomicU64:
input_tokens: AtomicU64output_tokens: AtomicU64cache_creation_tokens: AtomicU64cache_read_tokens: AtomicU64add_usage(input, output, cache_creation, cache_read)— atomic addstotal_cost_usd(model: &str) -> f64— loads atomics, looks up pricing by model substring matchsummary(model: &str) -> String— human-readable cost + token counts- Implements
Default
Module: hooks
HookContext struct: { event: String, tool_name: Option<String>, tool_input: Option<Value>, tool_output: Option<String>, is_error: Option<bool>, session_id: Option<String> }
HookOutcome enum: Allowed, Blocked(String), Modified(Value)
run_hooks(hooks, event, context, working_dir) -> HookOutcome (async):
- Iterates
Vec<HookEntry>for the givenHookEvent - Applies
tool_filter(glob match againsttool_name) - Spawns shell command via
tokio::process::Command - Sends
HookContextas JSON on stdin - If
blocking: trueand exit code != 0, returnsHookOutcome::Blocked(stderr) - Otherwise returns
HookOutcome::Allowed
Relationship to TypeScript
cc-core corresponds to the scattered TypeScript files: src/constants/, src/context.ts, src/history.ts, src/cost-tracker.ts, src/costHook.ts, src/schemas/hooks.ts, and parts of src/services/api/. The permission modes, hook events, and config structure mirror the TypeScript Config type exactly.
Crate: cc-api
Path: crates/api/src/lib.rs
Complete async Messages API client with SSE streaming support.
Module: types
CreateMessageRequest struct: built via CreateMessageRequestBuilder:
model: Stringmax_tokens: u32messages: Vec<ApiMessage>system: Option<SystemPrompt>tools: Option<Vec<ApiToolDefinition>>temperature: Option<f32>top_p: Option<f32>top_k: Option<u32>stop_sequences: Option<Vec<String>>thinking: Option<ThinkingConfig>stream: bool(always set totrueinternally)
CreateMessageRequestBuilder — fluent builder:
CreateMessageRequest::builder(model, max_tokens) -> Self.messages(Vec<ApiMessage>).system(SystemPrompt)or.system_text(String).tools(Vec<ApiToolDefinition>).temperature(f32),.top_p(f32),.top_k(u32).stop_sequences(Vec<String>).thinking(ThinkingConfig).build() -> CreateMessageRequest
ThinkingConfig: { type: "enabled", budget_tokens: u32 }
ThinkingConfig::enabled(budget: u32) -> Self
SystemPrompt enum (serde untagged):
Text(String)— simple text system promptBlocks(Vec<SystemBlock>)— structured blocks with cache control
SystemBlock: { type: "text", text: String, cache_control: Option<CacheControl> }
CacheControl: { type: "ephemeral" }
CacheControl::ephemeral() -> Self
ApiMessage: { role: String, content: Value }
From<&Message> for ApiMessage— convertscc_core::types::Messageto API format
ApiToolDefinition: { name: String, description: String, input_schema: Value, cache_control: Option<CacheControl> }
From<&ToolDefinition> for ApiToolDefinition- Last tool in the list gets
cache_control: Some(CacheControl::ephemeral())(prompt caching)
CreateMessageResponse: { id, type, role, content, model, stop_reason, stop_sequence, usage }
ApiErrorResponse: { type: String, error: ApiErrorDetail }
Module: streaming
StreamEvent enum (serde #[serde(tag = "type")]):
MessageStart { message: CreateMessageResponse }MessageDelta { delta: MessageDeltaData, usage: Option<StreamUsage> }MessageStopContentBlockStart { index: usize, content_block: PartialContentBlock }ContentBlockDelta { index: usize, delta: ContentDelta }ContentBlockStop { index: usize }PingError { error_type: String, message: String }
ContentDelta enum:
TextDelta { text: String }InputJsonDelta { partial_json: String }ThinkingDelta { thinking: String }SignatureDelta { signature: String }
StreamHandler trait:
fn on_event(&self, event: &StreamEvent)— called for each SSE event
NullStreamHandler — no-op implementation for headless mode
StreamAccumulator — collects stream events into a complete message:
on_event(&mut self, event: &StreamEvent)— processes all event typesfinish(self) -> (Message, UsageInfo, Option<String>)— returns (assistant_message, usage, stop_reason)
Internal PartialBlock enum during accumulation:
Text(String)ToolUse { id: String, name: String, json_buf: String }Thinking { thinking_buf: String, signature_buf: String }
Module: sse_parser
SseFrame struct: { event: Option<String>, data: Option<String> }
SseLineParser — stateful line-by-line SSE parser:
feed_line(&mut self, line: &str) -> Option<SseFrame>- Handles
event:,data:, and blank-line frame boundaries per SSE spec
Module: client
ClientConfig struct:
api_key: Stringapi_base: String(default:ANTHROPIC_API_BASE)timeout_secs: u64(default: 600)max_retries: u32(default: 5)
AnthropicClient struct:
AnthropicClient::new(config: ClientConfig) -> Result<Self>— validates API key, buildsreqwest::Clientwith rustls-tls, setsanthropic-versionandanthropic-betaheadersAnthropicClient::from_config(cfg: &Config) -> Result<Self>— resolves key/base from Config
create_message(request) -> Result<CreateMessageResponse> — non-streaming POST to /v1/messages
create_message_stream(request, handler) -> Result<mpsc::Receiver<StreamEvent>> (async):
- Sets
stream: trueon request - Spawns
tokio::spawnbackground task callingprocess_sse_stream() - Returns
mpsc::Receiver<StreamEvent>with channel buffer 256 - Background task reads response body line by line via
SseLineParser - Calls
frame_to_event()to parse each frame - Sends to channel + calls
handler.on_event()
send_with_retry(request_fn) -> Result<reqwest::Response> — exponential backoff:
- Max 5 retries
- Initial delay: 1 second
- Multiplier: 2× per retry, capped at 60 seconds
- Honors
Retry-Afterresponse header (overrides backoff delay) - Retries on 429 (
RateLimit) and 529 (ApiStatusoverloaded)
frame_to_event(frame: SseFrame) -> Option<StreamEvent> — dispatches by frame.event:
"ping"→StreamEvent::Ping"message_start"→ deserializedataintoStreamEvent::MessageStart"content_block_start"→StreamEvent::ContentBlockStart"content_block_delta"→StreamEvent::ContentBlockDelta"content_block_stop"→StreamEvent::ContentBlockStop"message_delta"→StreamEvent::MessageDelta"message_stop"→StreamEvent::MessageStop"error"→StreamEvent::Error
Relationship to TypeScript
Corresponds to src/services/api/claude.ts, src/services/api/client.ts, and src/services/api/errorUtils.ts. Implements the same SSE streaming protocol and retry logic. Prompt caching via CacheControl::ephemeral() mirrors the TypeScript cache control implementation. Beta header is identical.
Crate: cc-tools
Path: crates/tools/src/
Implements all 33 built-in tools. Each tool is a zero-sized struct implementing the Tool trait.
Core Types (lib.rs)
ToolResult struct:
content: Stringis_error: boolmetadata: Option<Value>— optional structured data for TUI renderingToolResult::success(content)/ToolResult::error(content)/.with_metadata(meta)
PermissionLevel enum: None, ReadOnly, Write, Execute, Dangerous
ToolContext struct:
working_dir: PathBufpermission_mode: PermissionModepermission_handler: Arc<dyn PermissionHandler>cost_tracker: Arc<CostTracker>session_id: Stringnon_interactive: boolmcp_manager: Option<Arc<cc_mcp::McpManager>>config: cc_core::config::Configresolve_path(&self, path: &str) -> PathBuf— resolves relative paths againstworking_dircheck_permission(tool_name, description, is_read_only) -> Result<(), ClaudeError>
Tool trait (async_trait):
fn name(&self) -> &strfn description(&self) -> &strfn permission_level(&self) -> PermissionLevelfn input_schema(&self) -> Value— JSON Schema for tool parametersasync fn execute(&self, input: Value, ctx: &ToolContext) -> ToolResultfn to_definition(&self) -> ToolDefinition— default impl from above methods
all_tools() -> Vec<Box<dyn Tool>> — returns all 33 tools
find_tool(name: &str) -> Option<Box<dyn Tool>> — finds by exact name match
Tool: BashTool (bash.rs)
Name: "Bash"
Permission level: Execute
Input schema: { command: string, timeout: optional u64 (seconds) }
Algorithm:
- Checks permission via
ctx.check_permission() - On Windows:
cmd /C <command>. On Unix:bash -c <command> - Default timeout: 120 seconds. Maximum: 600 seconds
- Collects stdout and stderr with
tokio::io::BufReader - Truncates output >100,000 characters with notice
- Non-zero exit →
ToolResult::errorwith combined stdout+stderr+exit_code - Zero exit →
ToolResult::successwith stdout (stderr appended if non-empty)
Tool: FileReadTool (file_read.rs)
Name: "Read"
Permission level: ReadOnly
Input schema: { file_path: string, offset: optional u32 (1-based line), limit: optional u32 }
Algorithm:
- Resolves path via
ctx.resolve_path() - Default limit: 2000 lines
- Reads entire file, splits on newlines
- Applies offset (1-based) and limit
- Formats output as
{line_number}\t{content} - Returns error on binary files (detected via
std::io::ErrorKind::InvalidData) - Returns stub message for images and PDFs
Tool: FileEditTool (file_edit.rs)
Name: "Edit"
Permission level: Write
Input schema: { file_path: string, old_string: string, new_string: string, replace_all: optional bool }
Algorithm:
- Validates
old_string != new_string - Reads current file content
- Counts occurrences of
old_string - If
replace_all == false(default) and count > 1: returns error (ambiguous) - If
replace_all == true: usesstr::replace()(replaces all) - If
replace_all == falseand count == 1: usesstr::replacen(old, new, 1) - Writes updated content back to file
Tool: FileWriteTool (file_write.rs)
Name: "Write"
Permission level: Write
Input schema: { file_path: string, content: string }
Algorithm:
- Resolves path
- Creates parent directories via
tokio::fs::create_dir_all() - Writes content to file
- Reports line count and byte count in success message
Tool: GlobTool (glob_tool.rs)
Name: "Glob"
Permission level: ReadOnly
Input schema: { pattern: string, path: optional string }
Algorithm:
- Resolves base path (defaults to
working_dir) - Constructs full glob pattern by joining base + pattern
- Uses
glob::glob()crate for pattern matching - Sorts results by modification time (most recent first)
- Returns max 250 results
- Returns newline-separated list of relative paths
Tool: GrepTool (grep_tool.rs)
Name: "Grep"
Permission level: ReadOnly
Input schema: { pattern: string, path: optional string, glob: optional string, type: optional string, output_mode: optional enum, context: optional u32, head_limit: optional u32, offset: optional u32, -i: optional bool, -n: optional bool, -A: optional u32, -B: optional u32, -C: optional u32, multiline: optional bool }
Algorithm:
- Compiles
RegexBuilderwithcase_insensitiveandmulti_lineflags - Uses
walkdir::WalkDirto traverse directory tree - Skips hidden directories,
node_modules/,target/,__pycache__/,.git/ - Filters by glob pattern or file type extension mapping
- Three output modes:
files_with_matches— list of file paths (default)content— matching lines with optional context (-A/-B/-C)count— match counts per file
- Applies
head_limitandoffsetpagination
Type shortcuts (e.g., type="js" → ["js", "jsx", "mjs", "cjs"]):
js,ts,py,rs,go,java,rb,cpp,c,cs,php,swift,kt,html,css,json,yaml,md
Tool: WebFetchTool (web_fetch.rs)
Name: "WebFetch"
Permission level: ReadOnly
Input schema: { url: string, prompt: optional string }
Algorithm:
reqwestGET with 30-second timeout, 10 redirect limit- User-Agent:
"Claude-Code/1.0" - If HTML content-type: runs
strip_html()— manual state machine removing tags, scripts, styles; converts&,<,>, entities - Truncates content >100,000 characters
- Returns text content
Tool: WebSearchTool (web_search.rs)
Name: "WebSearch"
Permission level: ReadOnly
Input schema: { query: string, num_results: optional u32 (default 5) }
Algorithm:
- Checks
BRAVE_SEARCH_API_KEYenv var:- If set: calls Brave Search API at
https://api.search.brave.com/res/v1/web/search - Returns title + URL + description for each result
- If set: calls Brave Search API at
- Fallback: DuckDuckGo Instant Answer API at
https://api.duckduckgo.com/?q=...&format=json - Returns up to
num_resultsformatted results
Tool: NotebookEditTool (notebook_edit.rs)
Name: "NotebookEdit"
Permission level: Write
Input schema: { notebook_path: string, cell_id: optional string, cell_index: optional u32, source: optional string, cell_type: optional string, mode: string (replace|insert|delete) }
Algorithm:
- Parses
.ipynbJSON withserde_json - Cell lookup: by UUID string OR by
cell-Nindex pattern - replace mode: updates
source, resetsoutputs = [],execution_count = null - insert mode: inserts new cell at given index or after cell_id; generates 8-char hex cell ID from
timestamp XOR random - delete mode: removes cell by id/index
- Writes updated notebook back to file
Tool: TaskCreateTool, TaskGetTool, TaskUpdateTool, TaskListTool, TaskStopTool, TaskOutputTool (tasks.rs)
Global store: TASK_STORE: Lazy<Arc<DashMap<String, Task>>>
Task struct: { id: String, subject: String, description: String, status: TaskStatus, owner: Option<String>, blocks: Vec<String>, blocked_by: Vec<String>, metadata: HashMap<String, Value>, output: Vec<String>, created_at: DateTime<Utc>, updated_at: DateTime<Utc> }
TaskStatus enum: Pending, InProgress, Completed, Deleted, Running, Failed
| Tool | Name | Description |
|---|---|---|
TaskCreateTool |
"TaskCreate" |
Creates task with UUID, stores in TASK_STORE |
TaskGetTool |
"TaskGet" |
Returns task JSON by ID |
TaskUpdateTool |
"TaskUpdate" |
Updates task fields; status=deleted removes from store |
TaskListTool |
"TaskList" |
Lists all non-deleted tasks with optional status filter |
TaskStopTool |
"TaskStop" |
Sets task status to Failed |
TaskOutputTool |
"TaskOutput" |
Appends text to task output vector |
Tool: CronCreateTool, CronDeleteTool, CronListTool (cron.rs)
Global store: CRON_STORE: Lazy<Arc<RwLock<HashMap<String, CronTask>>>>
CronTask struct: { id: String, cron: String, prompt: String, recurring: bool, durable: bool, created_at: DateTime<Utc> }
| Tool | Name | Description |
|---|---|---|
CronCreateTool |
"CronCreate" |
Creates scheduled task; validates cron expression; if durable=true, persists to .claude/scheduled_tasks.json; max 50 jobs |
CronDeleteTool |
"CronDelete" |
Removes task by ID from store (and disk if durable) |
CronListTool |
"CronList" |
Lists all scheduled tasks with human-readable schedule |
cron_matches(cron: &str, now: &DateTime<Local>) -> bool:
- Parses 5-field cron: minute, hour, day-of-month, month, day-of-week
- Supports:
*,*/N(step),N-M(range),N,M,...(list)
validate_cron(cron: &str) -> Result<()> — validates field ranges (minute 0–59, hour 0–23, etc.)
cron_to_human(cron: &str) -> String — describes schedule in plain English
pop_due_tasks() -> Vec<CronTask> — returns tasks matching current time, removes non-recurring tasks from store
Tool: TodoWriteTool (todo_write.rs)
Name: "TodoWrite"
Permission level: None
Input schema: { todos: Array<{ id: string, content: string, status: string, priority: string }> }
Replaces entire todo list. Returns summary with counts of pending/in_progress/completed items.
Tool: AskUserQuestionTool (ask_user.rs)
Name: "AskUserQuestion"
Permission level: None
Input schema: { question: string, options: optional Array<string> }
In non_interactive mode: returns error "Cannot prompt user in non-interactive mode".
Otherwise: returns ToolResult::success("") with metadata { type: "ask_user", question, options } for TUI layer to handle.
Tool: EnterPlanModeTool (enter_plan_mode.rs)
Name: "EnterPlanMode"
Permission level: None
Returns ToolResult::success with metadata { type: "enter_plan_mode" }. Signals the session to switch to Plan permission mode.
Tool: ExitPlanModeTool (exit_plan_mode.rs)
Name: "ExitPlanMode"
Permission level: None
Input schema: { summary: optional string }
Returns success with metadata { type: "exit_plan_mode", summary }. Signals return from Plan mode.
Tool: PowerShellTool (powershell.rs)
Name: "PowerShell"
Permission level: Execute
Input schema: { command: string, timeout: optional u64 }
Same execution pattern as BashTool. On Windows uses powershell -NoProfile -NonInteractive -Command. On other platforms uses pwsh.
Tool: EnterWorktreeTool, ExitWorktreeTool (worktree.rs)
Global: WORKTREE_SESSION: Lazy<Arc<RwLock<Option<WorktreeSession>>>>
WorktreeSession: { branch: String, path: PathBuf, original_dir: PathBuf }
EnterWorktreeTool ("EnterWorktree"):
- Input:
{ branch: string, path: optional string } - Runs
git worktree add -b <branch> <path> - Saves session to
WORKTREE_SESSION
ExitWorktreeTool ("ExitWorktree"):
- Input:
{ action: "keep" | "remove", discard_changes: optional bool } keep: locks worktree, clears sessionremove: checks for uncommitted changes (requiresdiscard_changes=trueto override), runsgit worktree remove --force <path>, thengit branch -D <branch>
Tool: SendMessageTool (send_message.rs)
Name: "SendMessage"
Permission level: None
Global: INBOX: Lazy<DashMap<String, Vec<AgentMessage>>>
Input schema: { to: string, message: string, metadata: optional Value }
- Delivers message to named recipient in
INBOX to = "*"broadcasts to all existing keysdrain_inbox(recipient: &str) -> Vec<AgentMessage>— removes and returns all messagespeek_inbox(recipient: &str) -> Vec<AgentMessage>— returns without removing
Tool: SkillTool (skill_tool.rs)
Name: "Skill"
Permission level: None
Input schema: { skill: string, arguments: optional string }
Algorithm:
skill = "list"→ enumerates.claude/commands/*.mdand~/.claude/commands/*.md, extracts description from YAML frontmatter or first heading- Otherwise: resolves
<skill>.mdfile from project then user commands directory - Strips YAML frontmatter (
---block) - Substitutes
$ARGUMENTSwith provided arguments string - Returns file content as
ToolResult::success
Tool: SleepTool (sleep.rs)
Name: "Sleep"
Permission level: None
Input schema: { duration: f64 (seconds) }
Calls tokio::time::sleep(Duration::from_secs_f64(duration)). Maximum 300 seconds.
Tool: ToolSearchTool (tool_search.rs)
Name: "ToolSearch"
Permission level: None
Input schema: { query: string, max_results: optional u32 (default 5) }
Static TOOL_CATALOG: &[(&str, &str, &[&str])] — 32 entries of (name, description, keywords).
Scoring algorithm:
select:Namesyntax → score 100 for exact name match- Otherwise for each catalog entry:
- exact name match: +20
- name contains query: +10
- description contains query: +5
- keyword exact match: +8
- keyword contains query: +3
- Returns top
max_resultsentries with non-zero score
Tool: BriefTool (brief.rs)
Name: "Brief"
Permission level: None
Input schema: { message: string, status: optional string, attachments: optional Array<string> (file paths) }
Resolves attachment metadata (file size, is_image flag from extension). Returns ToolResult::success("") with metadata { message, status, sentAt, attachments: [{ path, size, isImage }] }.
Tool: ConfigTool (config_tool.rs)
Name: "Config"
Permission level: None
Input schema: { action: "get" | "set", key: string, value: optional Value }
Reads/writes ~/.claude/settings.json. Supported keys: model, max_tokens, verbose, permission_mode, auto_compact. Returns current value on get, writes and confirms on set.
Tool: ListMcpResourcesTool, ReadMcpResourceTool (mcp_resources.rs)
| Tool | Name | Description |
|---|---|---|
ListMcpResourcesTool |
"ListMcpResources" |
Calls ctx.mcp_manager.list_all_resources(), returns JSON |
ReadMcpResourceTool |
"ReadMcpResource" |
Input: { uri: string }. Calls ctx.mcp_manager.read_resource(uri) |
Both return error if ctx.mcp_manager is None.
Relationship to TypeScript
cc-tools corresponds to the TypeScript tool implementations in src/ (e.g., bash is in the tool system, file operations in ReadTool/EditTool/WriteTool, etc.). Tool names are identical to the TypeScript constants. The ToolContext mirrors the TypeScript ToolUseContext.
Crate: cc-query
Path: crates/query/src/
The core agentic query loop crate. Contains 4 source files.
Module: lib.rs — Main Query Loop
QueryOutcome enum:
EndTurn { message: Message, usage: UsageInfo }— model issuedend_turnMaxTokens { partial_message: Message, usage: UsageInfo }— hit token limitCancelled— cancellation token firedError(ClaudeError)— unrecoverable error
QueryConfig struct:
model: Stringmax_tokens: u32max_turns: u32(default:MAX_TURNS_DEFAULT = 10)system_prompt: Option<String>append_system_prompt: Option<String>thinking_budget: Option<u32>temperature: Option<f32>QueryConfig::default()usesDEFAULT_MODEL+DEFAULT_MAX_TOKENSQueryConfig::from_config(cfg: &Config)— reads model + max_tokens from Config
QueryEvent enum:
Stream(StreamEvent)— raw API stream eventToolStart { tool_name, tool_id }— tool beginning executionToolEnd { tool_name, tool_id, result, is_error }— tool completedTurnComplete { turn: u32, stop_reason: String }— model turn finishedStatus(String)— informational messageError(String)— error notification
run_query_loop(client, messages, tools, tool_ctx, config, cost_tracker, event_tx, cancel_token) -> QueryOutcome (async):
Main agentic loop:
- Increments turn counter; returns
EndTurnif> max_turns - Checks
cancel_token.is_cancelled()→Cancelled - Converts
messages→Vec<ApiMessage>, tools →Vec<ApiToolDefinition> - Calls
build_system_prompt(config)to constructSystemPrompt - Builds
CreateMessageRequest(with thinking config ifbudgetprovided) - Creates
ChannelStreamHandlerorNullStreamHandler - Calls
client.create_message_stream(), receivesmpsc::Receiver<StreamEvent> - Inner loop:
tokio::select!on cancellation or stream events; feedsStreamAccumulator - On
MessageStopor channel close: callsaccumulator.finish() - Tracks costs via
cost_tracker.add_usage() - Appends assistant message to
messages - Calls
auto_compact_if_needed()if stop reason isend_turnortool_use - Dispatches on
stop_reason:"end_turn"/"stop_sequence"/ unknown → firesStophook → returnsEndTurn"max_tokens"→ returnsMaxTokens"tool_use"→ executes all tool_use blocks (see below), appends results,continue
Tool execution in tool_use turn:
- For each
ContentBlock::ToolUse { id, name, input }: - Emits
QueryEvent::ToolStart - Fires
PreToolUsehooks viacc_core::hooks::run_hooks(); ifHookOutcome::Blocked→ToolResult::error("Blocked by hook: ...") - Otherwise calls
execute_tool(name, input, tools, ctx) - Fires
PostToolUsehooks - Emits
QueryEvent::ToolEnd - Pushes
ContentBlock::ToolResultto result_blocks - Appends
Message::user_blocks(result_blocks)to conversation
execute_tool(name, input, tools, ctx) -> ToolResult (async):
- Finds tool by name in slice, calls
tool.execute(input, ctx) - Unknown tool →
ToolResult::error("Unknown tool: {name}")
build_system_prompt(config) -> SystemPrompt:
- Joins
system_promptandappend_system_promptwith\n\n - Empty → default
"You are Claude, an AI assistant by Anthropic."
run_single_query(client, messages, config) -> Result<Message> (async):
- Single API call, no tool loop,
NullStreamHandler - Returns complete assistant message
ChannelStreamHandler — implements StreamHandler:
on_event(&self, event)forwards tompsc::UnboundedSender<QueryEvent>
Module: compact.rs — Auto-Compact
Constants:
AUTOCOMPACT_BUFFER_TOKENS = 13_000WARNING_THRESHOLD_BUFFER_TOKENS = 20_000AUTOCOMPACT_TRIGGER_FRACTION = 0.90KEEP_RECENT_MESSAGES = 10MAX_CONSECUTIVE_FAILURES = 3
AutoCompactState struct:
compaction_count: u32consecutive_failures: u32disabled: bool— circuit breaker; set after 3 consecutive failures
TokenWarningState enum: Ok, Warning, Critical
context_window_for_model(model: &str) -> u32:
200_000for models matching "opus-4", "sonnet-4", "haiku-4", "claude-3-5"100_000otherwise
calculate_token_warning_state(input_tokens, model) -> TokenWarningState:
- Uses
WARNING_THRESHOLD_BUFFER_TOKENSto determine Warning vs Critical
should_auto_compact(state, input_tokens, model) -> bool:
- Returns false if
state.disabled - Returns true if
input_tokens / context_window > AUTOCOMPACT_TRIGGER_FRACTION
summarise_head(client, messages_to_summarize, model) -> Result<String> (async):
- Calls API with prompt asking to summarize the provided conversation
- Returns summary wrapped in
<compact-summary>...</compact-summary>XML tags
compact_conversation(client, messages, model) -> Result<Vec<Message>> (async):
- Splits conversation: head =
messages[0..total-KEEP_RECENT_MESSAGES], tail = last 10 messages - Calls
summarise_head()on head - Returns
[Message::user(summary)] + tail
auto_compact_if_needed(client, messages, input_tokens, model, state) -> Option<Vec<Message>> (async):
- Checks
should_auto_compact(), callscompact_conversation() - On success: resets
consecutive_failures, incrementscompaction_count - On failure: increments
consecutive_failures; disables if>= MAX_CONSECUTIVE_FAILURES - Returns
Some(new_messages)on success,Noneif not needed or failed
Module: agent_tool.rs — Sub-Agent Tool
AgentTool implements Tool:
- Name:
"Task"(constantTOOL_NAME_AGENT) - Permission level:
Execute
Input schema: { description: string, prompt: string, tools: optional Array<string>, system_prompt: optional string, max_turns: optional u32, model: optional string }
Algorithm:
- Creates dedicated
AnthropicClientfromANTHROPIC_API_KEYenv var - Filters tool list: if
toolsfield provided, uses that subset; always excludesTOOL_NAME_AGENT(prevents recursion) - Calls
run_query_loop()with:event_tx = None(no TUI forwarding for sub-agent)- New
ToolContextwith same working_dir, permission_mode, etc.
- Returns final assistant message text as
ToolResult::success()
Module: cron_scheduler.rs — Background Cron
start_cron_scheduler(tools, tool_ctx, cancel_token) -> JoinHandle<()>:
- Spawns
tokio::spawn(run_scheduler_loop(...))
run_scheduler_loop(tools, tool_ctx, cancel_token) (async loop):
- Computes seconds until next minute boundary:
sleep(60 - now.second() + 1) - Calls
cc_tools::cron::pop_due_tasks()to get matching tasks - For each due task: spawns
run_query_loop()with:- Single user message from
task.prompt event_tx = None(background, no UI)cancel_tokenclone
- Single user message from
- Loop continues until cancellation
Relationship to TypeScript
cc-query corresponds to src/query.ts, src/query/, src/services/compact/autoCompact.ts, src/coordinator/, and parts of src/services/autoDream/. The AgentTool corresponds to the TypeScript Task tool.
Crate: cc-tui
Path: crates/tui/src/lib.rs
Terminal UI built on ratatui + crossterm. Replaces the TypeScript ink/React rendering layer.
App struct
config: Config
cost_tracker: Arc<CostTracker>
messages: Vec<(Role, String)>
input: String
input_history: Vec<String>
history_index: Option<usize>
scroll_offset: u16
is_streaming: bool
streaming_text: String
status_message: Option<String>
should_quit: bool
show_help: bool
Key Methods
handle_key_event(&mut self, key: KeyEvent) -> Option<String>:
Ctrl+C: if streaming → cancels; if input empty → quits; else clears inputCtrl+D: if input empty → quits- Character input: appended to
self.input Backspace: removes last char fromself.inputEnter: returnsSome(input)for caller to process (empty input ignored)Up/Down: navigatesinput_historyPageUp/PageDown: adjustsscroll_offsetF1/?: toggles help overlay
handle_query_event(&mut self, event: QueryEvent):
Stream(ContentBlockDelta::TextDelta)→ appends tostreaming_textToolStart { tool_name, .. }→ setsstatus_message = "Running {tool_name}..."ToolEnd { .. }→ clearsstatus_messageTurnComplete { .. }→ movesstreaming_textintomessages, clears streaming stateStatus(msg)→ setsstatus_messageError(msg)→ setsstatus_messagewith error prefix
take_input(&mut self) -> String:
- Returns and clears
self.input - Pushes to
input_history(dedup at head)
add_message(&mut self, role: Role, text: String) — appends to messages vec
Module: render
render_app(f: &mut Frame, app: &App):
- Splits terminal into 3 vertical chunks via
Layout::vertical:- Messages area (flex fill)
- Input area (3 rows)
- Status bar (1 row)
- Messages: renders each
(role, text)pair —Role::Userin Cyan,Role::Assistantin Green. If streaming, appends partialstreaming_textin Yellow italic - Input area: bordered
Blocktitled "Input"; showsself.inputwith cursor_appended - Status bar: shows
{model} | {cost_summary}in Dark Gray
Module: widgets
render_permission_dialog(f: &mut Frame, question: &str, options: &[String]):
- Centered popup dialog
- Shows question text
- Lists numbered options
- Renders as
Clear+Block+Paragraphoverlay
render_spinner(f: &mut Frame, area: Rect, frame_count: u64):
- Cycles through braille spinner characters:
⠋⠙⠹⠸⠼⠴⠦⠧⠇⠏ - Indexed by
frame_count % 10
Module: input
is_slash_command(input: &str) -> bool — returns true if starts with "/"
parse_slash_command(input: &str) -> (&str, &str) — splits "/name args" → ("name", "args")
Terminal Setup
setup_terminal() -> Result<Terminal<CrosstermBackend<Stdout>>>:
enable_raw_mode()(crossterm)execute!(stdout, EnterAlternateScreen)(crossterm)- Creates
Terminal::new(CrosstermBackend::new(stdout))
restore_terminal(terminal: &mut Terminal<...>):
disable_raw_mode()execute!(stdout, LeaveAlternateScreen)terminal.show_cursor()
Relationship to TypeScript
cc-tui replaces the entire TypeScript src/ink/ rendering system, src/components/, and React/Ink component tree. The ratatui approach is fundamentally different (immediate-mode rendering vs React reconciler), but provides equivalent visual functionality: message history, streaming text, input box, status bar, permission dialogs.
Crate: cc-commands
Path: crates/commands/src/lib.rs
Slash command implementations for the interactive REPL.
Core Types
CommandContext struct:
config: Config
cost_tracker: Arc<CostTracker>
messages: Vec<Message>
working_dir: PathBuf
CommandResult enum:
Message(String)— display text to userUserMessage(String)— inject as user message into conversationConfigChange(Config)— update running configClearConversation— clear message historySetMessages(Vec<Message>)— replace message historyExit— terminate sessionSilent— no outputError(String)— display error
SlashCommand trait (async_trait):
fn name(&self) -> &strfn aliases(&self) -> Vec<&str>(default: empty)fn description(&self) -> &strfn help(&self) -> &strfn hidden(&self) -> bool(default: false)async fn execute(&self, args: &str, ctx: &CommandContext) -> CommandResult
Command Registry
all_commands() -> Vec<Box<dyn SlashCommand>> — returns all built-in commands
find_command(name: &str) -> Option<Box<dyn SlashCommand>> — matches by name or alias
execute_command(input: &str, ctx: &CommandContext) -> Option<CommandResult> (async):
- Parses slash command from input
- Finds matching command
- Returns
Noneif no match (pass-through to query loop)
Implemented Commands
| Struct | Name | Aliases | Description |
|---|---|---|---|
HelpCommand |
help |
h, ? |
List available slash commands |
ClearCommand |
clear |
cls |
Clear conversation history |
CompactCommand |
compact |
— | Manually compact conversation |
CostCommand |
cost |
— | Show current session cost |
ExitCommand |
exit |
quit, q |
Exit the REPL |
ModelCommand |
model |
— | Show/change current model |
ConfigCommand |
config |
— | Show/update configuration |
VersionCommand |
version |
— | Show version information |
ResumeCommand |
resume |
— | Resume previous conversation |
StatusCommand |
status |
— | Show session status |
DiffCommand |
diff |
— | Show file diffs |
MemoryCommand |
memory |
— | Manage CLAUDE.md memories |
BugCommand |
bug |
— | File a bug report |
DoctorCommand |
doctor |
— | Run diagnostics |
LoginCommand |
login |
— | Authenticate |
LogoutCommand |
logout |
— | Clear authentication |
InitCommand |
init |
— | Initialize project CLAUDE.md |
ReviewCommand |
review |
— | Code review workflow |
HooksCommand |
hooks |
— | Manage event hooks |
McpCommand |
mcp |
— | Manage MCP servers |
PermissionsCommand |
permissions |
— | Show/edit permissions |
PlanCommand |
plan |
— | Enter/exit plan mode |
TasksCommand |
tasks |
— | View background tasks |
SessionCommand |
session |
— | Session management |
ThinkingCommand |
thinking |
— | Toggle extended thinking |
ExportCommand |
export |
— | Export conversation |
SkillsCommand |
skills |
— | List/manage skills |
RewindCommand |
rewind |
— | Rewind conversation state |
StatsCommand |
stats |
— | Show usage statistics |
FilesCommand |
files |
— | List context files |
RenameCommand |
rename |
— | Rename current session |
EffortCommand |
effort |
— | Set effort/thinking level |
SummaryCommand |
summary |
— | Summarize conversation |
CommitCommand |
commit |
— | Run git commit workflow |
Relationship to TypeScript
cc-commands corresponds to the TypeScript src/commands/ directory (150+ files). Each TypeScript command module (e.g., src/commands/compact/, src/commands/model/) maps to a struct in this crate. The slash command names and behaviors are preserved.
Crate: cc-mcp
Path: crates/mcp/src/lib.rs
Full MCP (Model Context Protocol) client implementation. Uses JSON-RPC 2.0 over stdio subprocess transport.
JSON-RPC Types
JsonRpcRequest: { jsonrpc: "2.0", method: String, params: Option<Value>, id: Option<u64> }
JsonRpcRequest::new(method, params, id)— regular requestJsonRpcRequest::notification(method, params)— no id
JsonRpcResponse: { jsonrpc: "2.0", id: Option<u64>, result: Option<Value>, error: Option<JsonRpcError> }
JsonRpcError: { code: i32, message: String, data: Option<Value> }
MCP Protocol Types
InitializeParams: { protocol_version: "2024-11-05", capabilities: ClientCapabilities, client_info: ClientInfo }
ClientCapabilities: { roots: Option<RootsCapability> }
InitializeResult: { protocol_version: String, capabilities: ServerCapabilities, server_info: ServerInfo }
ServerCapabilities: { tools: Option<ToolsCapability>, resources: Option<ResourcesCapability>, prompts: Option<PromptsCapability> }
McpTool: { name: String, description: Option<String>, input_schema: Value }
From<&McpTool> for ToolDefinition— converts to cc-core ToolDefinition
CallToolParams: { name: String, arguments: Option<Value> }
CallToolResult: { content: Vec<McpContent>, is_error: Option<bool> }
McpContent enum (serde tagged):
Text { type: "text", text: String }Image { type: "image", data: String, mime_type: String }Resource { type: "resource", resource: ResourceContents }
McpResource: { uri: String, name: String, description: Option<String>, mime_type: Option<String> }
McpPrompt: { name: String, description: Option<String>, arguments: Option<Vec<McpPromptArgument>> }
Transport
McpTransport trait (async_trait):
async fn send(&mut self, request: &JsonRpcRequest) -> Result<()>async fn recv(&mut self) -> Result<Option<JsonRpcResponse>>async fn close(&mut self)
StdioTransport:
StdioTransport::spawn(command: &str, args: &[String], env: &HashMap<String, String>) -> Result<Self>- Spawns subprocess with piped stdin/stdout
- Spawns background reader task forwarding lines to
mpsc::UnboundedReceiver<String>
send()— serializes to JSON + newline on stdinrecv()— receives from channel, deserializes JSON-RPC response
McpClient
McpClient::connect_stdio(config: &McpServerConfig) -> Result<Self> (async):
- Calls
StdioTransport::spawn() - Calls
initialize()— sendsinitializerequest, receivesInitializeResult - Sends
notifications/initializednotification - If
capabilities.toolspresent: callstools/list, stores inself.tools - If
capabilities.resourcespresent: callsresources/list, stores - If
capabilities.promptspresent: callsprompts/list, stores - Returns connected client
call<T: DeserializeOwned>(&mut self, method, params) -> Result<T>:
- Sequential request/response: sends request with incrementing ID
- Calls
transport.recv()in loop until response ID matches - Deserializes
resultfield
call_tool(&mut self, name: &str, arguments: Option<Value>) -> Result<CallToolResult>:
- Calls
tools/callwithCallToolParams
list_resources(&mut self) -> Result<Vec<McpResource>> — resources/list
read_resource(&mut self, uri: &str) -> Result<ResourceContents> — resources/read
McpManager
Manages multiple named MCP server connections.
McpManager::connect_all(configs: &[McpServerConfig]) -> Result<Self> (async):
- Attempts to connect each server; logs warnings on failure (doesn't abort)
all_tool_definitions(&self) -> Vec<ToolDefinition>:
- Prefixes each tool name with
"{server_name}_"to namespace tools
call_tool(&self, prefixed_name: &str, arguments: Option<Value>) -> Result<CallToolResult>:
- Strips server prefix to identify server
- Routes to correct
McpClient
list_all_resources(&self) -> Result<Vec<McpResource>>:
- Aggregates resources from all connected servers
read_resource(&self, uri: &str) -> Result<ResourceContents>:
- Tries each server until one returns a result
server_count(&self) -> usize, server_names(&self) -> Vec<String>
mcp_result_to_string(result: &CallToolResult) -> String:
- Converts
McpContent::Text→ text,McpContent::Image→[image: mime_type],McpContent::Resource→ URI/text
Relationship to TypeScript
cc-mcp corresponds to src/services/mcpClient.ts (TypeScript MCP implementation). Implements the same MCP protocol version (2024-11-05), stdio transport, and tool namespacing convention.
Crate: cc-bridge
Path: crates/bridge/src/lib.rs
Implements the bridge protocol connecting the local Claude Code CLI to the claude.ai web UI. Enables remote control of the CLI from a browser session.
Configuration
BridgeConfig struct:
enabled: boolserver_url: Stringdevice_id: Stringsession_token: Option<String>polling_interval_ms: u64(default: 1000)max_reconnect_attempts: u32(default: 10)
Protocol Types
BridgeMessage enum (serde tagged — messages from server to client):
UserMessage { content: String, attachments: Vec<String> }PermissionResponse { tool_use_id: String, decision: PermissionDecision }CancelPing
BridgeEvent enum (serde tagged — events from client to server):
TextDelta { text: String }ToolStart { tool_name: String, tool_id: String }ToolEnd { tool_name: String, tool_id: String, result: String, is_error: bool }PermissionRequest { tool_use_id: String, tool_name: String, description: String }TurnComplete { stop_reason: String }Error { message: String }Pong
PermissionDecision enum: Allow, AllowPermanently, Deny, DenyPermanently
BridgeState enum: Connecting, Connected, Reconnecting { attempt: u32 }, Disconnected
Session Management
BridgeSession::new(config: BridgeConfig) -> (Self, mpsc::Receiver<BridgeMessage>, mpsc::Sender<BridgeEvent>):
- Creates channel pair for bidirectional communication
BridgeManager::start(config, msg_tx, event_rx) -> Self:
- Spawns
run_poll_loop()background task - Returns manager with
JoinHandle
Polling Loop
run_poll_loop(config, msg_tx, event_rx) (async):
- Long-polls
{server_url}/sessions/{id}/pollwithreqwestGET - On response: deserializes
BridgeMessagearray, sends each tomsg_tx - Drains
event_rx: sends accumulatedBridgeEventitems to{server_url}/sessions/{id}/eventsvia POST - On network error: exponential backoff up to
max_reconnect_attempts - On 401/403: sets state to
Disconnected, exits loop
Module: jwt
JwtClaims struct: { sub: String, exp: u64, iat: u64, device_id: String }
decode_payload(token: &str) -> Result<JwtClaims>:
- Splits token by
".", takes index 1 (payload segment) - Base64 decodes (URL-safe, no padding) via
base64crate - Deserializes JSON to
JwtClaims
is_expired(claims: &JwtClaims) -> bool:
- Compares
claims.expagainstSystemTime::now()Unix timestamp
Module: trusted_device
device_fingerprint() -> String:
- Collects:
hostname()(fromhostnamecrate),USERenv var, home directory path - SHA-256 hash of concatenated string via
sha2crate - Returns lowercase hex string (first 16 chars) via
hexcrate
Relationship to TypeScript
cc-bridge corresponds to src/bridge/ (31 TypeScript files including bridgeMain.ts, bridgeMessaging.ts, replBridge.ts, jwtUtils.ts, trustedDevice.ts, etc.). Implements the same polling-based bridge protocol and JWT handling.
Crate: claude-code (CLI Binary)
Path: crates/cli/src/main.rs
Binary entry point. Produces the claude executable. Wires all crates together.
CLI Arguments (Cli struct via clap derive)
| Flag | Type | Description |
|---|---|---|
prompt |
Option<String> (positional) |
Non-interactive prompt |
-p, --print |
bool |
Print mode (alias for non-interactive) |
-m, --model |
Option<String> |
Override model |
--permission-mode |
Option<CliPermissionMode> |
Permission mode |
--resume |
Option<String> |
Resume session by ID |
--max-turns |
u32 (default: 10) |
Max conversation turns |
-s, --system-prompt |
Option<String> |
Override system prompt |
--append-system-prompt |
Option<String> |
Append to system prompt |
--no-claude-md |
bool |
Skip CLAUDE.md loading |
--output-format |
Option<CliOutputFormat> |
Output format |
-v, --verbose |
bool |
Enable verbose logging |
--api-key |
Option<String> |
API key |
--max-tokens |
Option<u32> |
Override max tokens |
--cwd |
Option<PathBuf> |
Working directory |
--dangerously-skip-permissions |
bool |
BypassPermissions mode |
--dump-system-prompt |
bool |
Print system prompt and exit |
--mcp-config |
Option<PathBuf> |
MCP server config JSON file |
--no-auto-compact |
bool |
Disable auto-compact |
CliPermissionMode enum (clap ValueEnum): Default, AcceptEdits, BypassPermissions, Plan
CliOutputFormat enum (clap ValueEnum): Text, Json, StreamJson
McpToolWrapper
Implements Tool for tools provided by MCP servers:
permission_level()→Executeexecute()strips server prefix from tool name, callsMcpManager::call_tool(), converts result viamcp_result_to_string()
main() Function
- Parses
Cliargs with clap - Sets up
tracing_subscriber(verbose → DEBUG, default → WARN) - Loads
Settingsfrom~/.claude/settings.json - Builds
Configby layering: settings → CLI overrides - Determines
working_dir(from--cwdorstd::env::current_dir()) - Creates
Arc<CostTracker> - Builds system context strings:
- Reads
crates/cli/src/system_prompt.txt(embedded at compile time viainclude_str!) - Calls
ContextBuilder::build_system_context() - Calls
ContextBuilder::build_user_context()(unless--no-claude-md) - Joins all parts
- Reads
- If
--dump-system-prompt: prints and exits - Creates
AnthropicClient::from_config() - Creates
ToolContextwithAutoPermissionHandler - Calls
McpManager::connect_all()if MCP config provided - Builds tool list:
cc_tools::all_tools()+AgentTool+McpToolWrapperfor each MCP tool - Creates
CancellationToken, starts cron scheduler withstart_cron_scheduler() - If prompt provided or
--print: callsrun_headless() - Otherwise: calls
run_interactive()
run_headless(prompt, client, messages, tools, tool_ctx, config, cost_tracker, output_format)
- Reads prompt from arg or stdin (if no positional arg)
- Pushes
Message::user(prompt)to messages - Spawns
run_query_loop()withmpsc::unbounded_channel()for events - Drains event channel:
Textoutput format: printsQueryEvent::Stream(TextDelta)text directly; prints tool namesJsonformat: collects full response, outputs as single JSON objectStreamJsonformat: outputs eachQueryEventas NDJSON line
- Returns on
QueryOutcome::EndTurnor error
run_interactive()
Interactive TUI REPL:
- Sets up terminal via
cc_tui::setup_terminal() - Restores terminal on exit (via
defer-pattern) - Handles session resume if
--resumeprovided - Main event loop at 16ms poll interval (
EventStreamfrom crossterm):- Processes
crossterm::event::KeyEventviaapp.handle_key_event() - On Enter: if slash command (
is_slash_command()), callsexecute_command()fromcc-commands - Regular message: pushes to
messages, spawnsrun_query_loop()astokio::spawn - Shares
Arc<Mutex<Vec<Message>>>between main and spawned task for result sync - Drains query events via
event_rx.try_recv() - Calls
app.handle_query_event()to update TUI state - Re-renders via
terminal.draw(|f| render_app(f, &app))
- Processes
- Saves session to
cc_core::history::save_session()after each completed turn
System Prompt (system_prompt.txt)
Embedded in binary at compile time. Content:
You are Claude Code, an AI coding assistant by Anthropic.
Guidelines:
- Read files before editing them
- Prefer editing existing files over creating new ones
- Write clean, idiomatic code
- Run tests after making changes
- Use git log/diff for codebase context
- Be concise in responses
- Produce production-quality code
- Never introduce security vulnerabilities
Relationship to TypeScript
claude-code CLI corresponds to src/entrypoints/cli.tsx (the main TypeScript CLI entry point), src/main.tsx, src/screens/REPL.tsx, and src/cli/ directory. The CLI flag names and behaviors are preserved, including --print, --output-format, --permission-mode, and --resume.
Cross-Cutting Architecture Notes
Async Runtime
All async code uses tokio with "full" features. The #[tokio::main] macro is on main() in crates/cli/src/main.rs. All tools use async fn execute() via async_trait.
Cancellation
tokio_util::sync::CancellationToken is threaded through run_query_loop(), the cron scheduler, and TUI event loop. Ctrl+C fires the token.
Global State
Three DashMap/RwLock singletons using once_cell::sync::Lazy:
TASK_STORE(cc-tools/tasks.rs) — task managementINBOX(cc-tools/send_message.rs) — inter-agent messagingCRON_STORE(cc-tools/cron.rs) — scheduled tasksWORKTREE_SESSION(cc-tools/worktree.rs) — active git worktree
Error Handling
- Libraries use
thiserrorfor typedClaudeError - CLI binary uses
anyhowfor ergonomic error propagation - Tool errors never panic; always return
ToolResult::error()
Prompt Caching
cc-api automatically applies CacheControl::ephemeral() to:
- System prompt blocks (when using
SystemPrompt::Blocks) - The last tool definition in the tools list
Logging
tracing + tracing-subscriber with EnvFilter. Default level WARN; --verbose enables DEBUG. Structured fields on all log calls.
TypeScript Parity Summary
| TypeScript Area | Rust Crate |
|---|---|
src/entrypoints/cli.tsx, src/main.tsx |
crates/cli |
src/services/api/ |
crates/api |
src/query.ts, src/query/ |
crates/query |
src/components/, src/ink/ |
crates/tui |
src/commands/ |
crates/commands |
src/constants/, src/context.ts, etc. |
crates/core |
| Tool implementations (Bash, Read, Edit, etc.) | crates/tools |
MCP client (src/services/mcpClient.ts) |
crates/mcp |
src/bridge/ |
crates/bridge |