Skip to main content
The tool system lives in the cc-tools crate. It defines the Tool trait, a registry of 33 built-in tools, and the ToolContext struct that carries runtime state into every tool call.

The Tool trait

Every tool is a zero-sized struct that implements the Tool trait, defined in crates/tools/src/lib.rs:
#[async_trait]
pub trait Tool: Send + Sync {
    fn name(&self) -> &str;
    fn description(&self) -> &str;
    fn permission_level(&self) -> PermissionLevel;
    fn input_schema(&self) -> Value;   // JSON Schema for tool parameters
    async fn execute(&self, input: Value, ctx: &ToolContext) -> ToolResult;

    // Default implementation — builds a ToolDefinition from the above methods
    fn to_definition(&self) -> ToolDefinition {
        ToolDefinition {
            name: self.name().to_owned(),
            description: self.description().to_owned(),
            input_schema: self.input_schema(),
        }
    }
}

PermissionLevel

Each tool declares a PermissionLevel that controls which permission modes will allow it to run automatically:
LevelMeaning
NoneNo permission check required (metadata tools, signalling tools)
ReadOnlySafe to run automatically in Default and AcceptEdits modes
WriteRequires user approval in Default mode; automatic in AcceptEdits
ExecuteRuns shell commands; requires explicit approval or BypassPermissions
DangerousHighest risk; denied in Plan mode

ToolResult

pub struct ToolResult {
    pub content: String,
    pub is_error: bool,
    pub metadata: Option<Value>,   // Optional structured data for TUI rendering
}

impl ToolResult {
    pub fn success(content: impl Into<String>) -> Self { ... }
    pub fn error(content: impl Into<String>) -> Self { ... }
    pub fn with_metadata(self, meta: Value) -> Self { ... }
}

ToolContext

ToolContext is passed to every execute() call and carries all runtime state a tool needs:
pub struct ToolContext {
    pub working_dir: PathBuf,
    pub permission_mode: PermissionMode,
    pub permission_handler: Arc<dyn PermissionHandler>,
    pub cost_tracker: Arc<CostTracker>,
    pub session_id: String,
    pub non_interactive: bool,
    pub mcp_manager: Option<Arc<McpManager>>,
    pub config: Config,
}
ctx.resolve_path(path) resolves relative paths against working_dir. ctx.check_permission(tool_name, description, is_read_only) runs the permission check and returns Err(ClaudeError::PermissionDenied(...)) if access is denied.

Input schema validation

Tool input schemas are generated using schemars (derive-based) and serialized to serde_json::Value. The schemas follow JSON Schema draft-07. The query loop validates tool inputs against these schemas before calling execute(). The tool schema cache: ToolDefinition structs produced by to_definition() are collected once at startup into a Vec<ApiToolDefinition> and reused across turns. The last tool in the list automatically receives a CacheControl::ephemeral() annotation from cc-api, enabling prompt caching on the tools block.

Registry functions

// Returns all 33 built-in tools
pub fn all_tools() -> Vec<Box<dyn Tool>>

// Finds a tool by its exact name constant
pub fn find_tool(name: &str) -> Option<Box<dyn Tool>>

Tool categories and names

Tool namePermissionDescription
ReadReadOnlyRead file contents with optional line offset and limit
WriteWriteCreate or overwrite a file; creates parent directories automatically
EditWriteReplace exact string in a file; replace_all flag for bulk replacement
NotebookEditWriteEdit Jupyter .ipynb cells: replace, insert, or delete
Tool namePermissionDescription
BashExecuteRun a shell command via bash -c (or cmd /C on Windows); default timeout 120s, max 600s
PowerShellExecuteRun a PowerShell command; uses pwsh on non-Windows
Tool namePermissionDescription
GlobReadOnlyFind files by glob pattern, sorted by modification time; max 250 results
GrepReadOnlySearch file contents with a regex; three output modes: files_with_matches, content, count
Tool namePermissionDescription
TaskExecuteSpawn a sub-agent that runs its own run_query_loop(); returns the final assistant message
Tool namePermissionDescription
WebFetchReadOnlyHTTP GET with HTML stripping; 30s timeout; 100K character limit
WebSearchReadOnlySearch via Brave Search API (with BRAVE_SEARCH_API_KEY) or DuckDuckGo fallback
Tool namePermissionDescription
ListMcpResourcesReadOnlyList all resources from connected MCP servers
ReadMcpResourceReadOnlyRead a resource by URI from a connected MCP server
Tool namePermissionDescription
TaskCreateNoneCreate a task in the global TASK_STORE with a UUID
TaskGetNoneRetrieve a task by ID
TaskUpdateNoneUpdate task fields; status=deleted removes from store
TaskListNoneList all non-deleted tasks, with optional status filter
TaskStopNoneSet a task’s status to Failed
TaskOutputNoneAppend text to a task’s output vector
TodoWriteNoneReplace the entire todo list atomically
Tool namePermissionDescription
CronCreateNoneCreate a scheduled task with a 5-field cron expression; max 50 jobs
CronDeleteNoneDelete a scheduled task by ID
CronListNoneList all scheduled tasks with human-readable schedules
Tool namePermissionDescription
EnterPlanModeNoneSwitch the session to Plan (read-only) permission mode
ExitPlanModeNoneReturn from plan mode; accepts an optional summary
Tool namePermissionDescription
EnterWorktreeNoneCreate a git worktree on a new branch and switch the session to it
ExitWorktreeNoneReturn from a worktree session; keep or remove the worktree
Tool namePermissionDescription
ToolSearchNoneFind tools by keyword with a scoring algorithm; select:Name for exact lookup
AskUserQuestionNoneSurface a question to the user in the TUI; returns error in non-interactive mode
SendMessageNoneDeliver a message to a named recipient in the shared INBOX map
BriefNoneAttach files and a status message for TUI rendering
SleepNoneAsync delay up to 300 seconds
ConfigNoneRead or write fields in ~/.claude/settings.json
SkillNoneLoad and execute a skill from .claude/commands/ or ~/.claude/commands/

Simplified tool implementation example

The following example is representative of how a read-only tool is structured, based on the pattern used throughout cc-tools:
use async_trait::async_trait;
use serde_json::Value;

pub struct GlobTool;

#[async_trait]
impl Tool for GlobTool {
    fn name(&self) -> &str {
        "Glob"
    }

    fn description(&self) -> &str {
        "Find files by name pattern. Returns paths sorted by modification time."
    }

    fn permission_level(&self) -> PermissionLevel {
        PermissionLevel::ReadOnly
    }

    fn input_schema(&self) -> Value {
        serde_json::json!({
            "type": "object",
            "properties": {
                "pattern": {
                    "type": "string",
                    "description": "Glob pattern (e.g. \"**/*.rs\")"
                },
                "path": {
                    "type": "string",
                    "description": "Directory to search in (defaults to working directory)"
                }
            },
            "required": ["pattern"]
        })
    }

    async fn execute(&self, input: Value, ctx: &ToolContext) -> ToolResult {
        let pattern = input["pattern"].as_str().unwrap_or("");
        let base = input["path"]
            .as_str()
            .map(|p| ctx.resolve_path(p))
            .unwrap_or_else(|| ctx.working_dir.clone());

        let full_pattern = base.join(pattern).to_string_lossy().into_owned();
        let mut results: Vec<_> = glob::glob(&full_pattern)
            .into_iter()
            .flatten()
            .flatten()
            .take(250)
            .collect();

        // Sort by modification time, most recent first
        results.sort_by_key(|p| {
            std::fs::metadata(p)
                .and_then(|m| m.modified())
                .unwrap_or(std::time::SystemTime::UNIX_EPOCH)
        });
        results.reverse();

        let output = results
            .iter()
            .map(|p| p.display().to_string())
            .collect::<Vec<_>>()
            .join("\n");

        ToolResult::success(output)
    }
}
Tool names in cc-tools match the TypeScript constants exactly (e.g. "Bash", "Read", "Edit", "Task"). This ensures full compatibility with any system that references tools by name.

Build docs developers (and LLMs) love