Claude Code’s source code leaked. Setting aside the surveillance concerns and the inevitable spaghetti of any real codebase, it’s a genuinely well-designed harness.
I’ve been digging through it, picking out patterns worth understanding. This is one of them: the tool system that makes every capability pluggable.
The Tool Count Paradox
When OpenClaw went viral, everyone talked about Pi theory and “minimal tools design” - the idea that autonomous agents should have as few tools as possible to reduce complexity and failure modes.
Claude Code ships with 45+ built-in tools.
And it works. Really well.
This doesn’t disprove minimal tools theory. But it complicates the narrative. If OpenClaw’s philosophy is correct, Claude Code should be a mess. It isn’t. So what’s going on?
We don’t know the true reason for Claude Code’s design - we’re reading leaked code, not talking to designers. But we can observe what’s there.
The Tool Inventory
45+ tools sounds chaotic. Here’s what it actually looks like:
- File Operations:
Read,Edit,Write,Glob,Grep,NotebookEdit - Execution:
Bash,PowerShell - Agent Orchestration:
Agent,TaskOutput,TaskStop,TaskCreate,TaskGet,TaskUpdate,TaskList,TeamCreate,TeamDelete - User Interaction:
AskUserQuestion,SendUserMessage - Mode Management:
EnterPlanMode,ExitPlanMode,EnterWorktree,ExitWorktree - Web & Network:
WebFetch,WebSearch - MCP Integration:
mcp__*(dynamic),ListMcpResourcesTool,ReadMcpResourceTool - System & Scheduling:
Config,Skill,LSP,ToolSearch,TodoWrite,CronCreate,CronDelete,CronList,RemoteTrigger
Plus internal tools: REPL, Tungsten, Sleep, SendMessage, Workflow, etc. And MCP tools loaded dynamically from external servers.
The Inventory is Dynamic
The 45+ count isn’t a fixed list. Different execution contexts get different tool sets.
// src/constants/tools.ts
export const ASYNC_AGENT_ALLOWED_TOOLS = new Set([
FILE_READ_TOOL_NAME,
WEB_SEARCH_TOOL_NAME,
GREP_TOOL_NAME,
GLOB_TOOL_NAME,
SHELL_TOOL_NAMES,
FILE_EDIT_TOOL_NAME,
FILE_WRITE_TOOL_NAME,
NOTEBOOK_EDIT_TOOL_NAME,
SKILL_TOOL_NAME,
SYNTHETIC_OUTPUT_TOOL_NAME,
TOOL_SEARCH_TOOL_NAME,
ENTER_WORKTREE_TOOL_NAME,
EXIT_WORKTREE_TOOL_NAME,
// ~15 tools
])
export const ALL_AGENT_DISALLOWED_TOOLS = new Set([
AGENT_TOOL_NAME, // No recursion
ASK_USER_QUESTION_TOOL_NAME, // No blocking
TASK_STOP_TOOL_NAME, // No cross-agent control
ENTER_PLAN_MODE_TOOL_NAME,
EXIT_PLAN_MODE_V2_TOOL_NAME,
])
export const COORDINATOR_MODE_ALLOWED_TOOLS = new Set([
AGENT_TOOL_NAME,
TASK_STOP_TOOL_NAME,
SEND_MESSAGE_TOOL_NAME,
SYNTHETIC_OUTPUT_TOOL_NAME, // Only 4 tools
])
Three contexts, three tool sets.
- Main REPL: ~45+ tools with filtering
- Async Agents: ~15 tools, no recursion/blocking
- Coordinator mode: 4 tools, orchestration only
The filtering is applied at runtime.
// src/utils/toolPool.ts
export function mergeAndFilterTools(
initialTools: Tools,
assembled: Tools,
mode: ToolPermissionContext['mode'],
): Tools {
// Merge and deduplicate ~45+ tools
const tools = [...builtIn.sort(byName), ...mcp.sort(byName)]
// Apply coordinator mode filter → 4 tools
if (feature('COORDINATOR_MODE') && coordinatorModeModule?.isCoordinatorMode()) {
return applyCoordinatorToolFilter(tools)
}
return tools // Return all ~45+ for REPL, filtered set for async agents
}
The real number won’t be 45, but still a relatively large number compared to the 4 tools mentioned in OpenClaw and Pi theory.
The Tool Has a Fat Interface
The tool system supports a vast number of functionalities, and ends up with a fat interface. Due to the amount of dynamic import in the codebase to solve circular import, we can argue the fat interface is tech debt—the tool system is handling way more than its initial design.
// src/Tool.ts (simplified)
type Tool<Input, Output> = {
// Identity - 5 fields
readonly name: string
readonly inputSchema: Input
readonly outputSchema?: ZodType
readonly shouldDefer?: boolean
readonly alwaysLoad?: boolean
// Execution - 2 methods
call(args, context, canUseTool, parent, onProgress): Promise<ToolResult<Output>>
description(input, options): Promise<string>
// Permissions - 3 methods
validateInput?(input, context): Promise<ValidationResult>
checkPermissions(input, context): Promise<PermissionResult>
preparePermissionMatcher?(input): Promise<(pattern: string) => boolean>
// Capability flags - 8 methods
isEnabled(): boolean
isReadOnly(input): boolean
isConcurrencySafe(input): boolean
isDestructive?(input): boolean
isOpenWorld?(input): boolean
requiresUserInteraction?(): boolean
isMcp?: boolean
isLsp?: boolean
// UI/UX - 10+ methods
userFacingName(input): string
userFacingNameBackgroundColor?(input): string | undefined
getActivityDescription?(input): string | null
getToolUseSummary?(input): string | null
renderToolUseMessage?(...): ReactNode
renderToolResultMessage?(...): ReactNode
renderToolUseErrorMessage?(...): ReactNode
isSearchOrReadCommand?(input): { isSearch: boolean; isRead: boolean }
// ... more ...
// Advanced - 15+ niche methods
backfillObservableInput?(input): void
getPath?(input): string
interruptBehavior?(): 'cancel' | 'block'
mapToolResultToToolResultBlockParam?(...): ToolResultBlockParam
// ... more ...
}
40+ methods/properties. While a typical tool only uses a minor subset of them. GlobTool only uses 14.
To make the tool definition easier, buildTool is introduced with a bunch of defaulted methods implemented.
// src/Tool.ts
const TOOL_DEFAULTS = {
isEnabled: () => true,
isConcurrencySafe: (_input?: unknown) => false, // Conservative
isReadOnly: (_input?: unknown) => false,
isDestructive: (_input?: unknown) => false,
checkPermissions: (input, _ctx) =>
Promise.resolve({ behavior: 'allow', updatedInput: input }),
toAutoClassifierInput: (_input?: unknown) => '',
userFacingName: (_input?: unknown) => '',
}
export function buildTool<D extends AnyToolDef>(def: D): BuiltTool<D> {
return { ...TOOL_DEFAULTS, ...def }
}
Permission Layers
Tools have permission layers built as part of the interface.
validateInput()- Tool-specific validation
- Model gets actionable feedback and can retry
checkPermissions()- Tool-specific rules (different from validation)
- Example: Read tool blocks device files
canUseTool()- General permission system (rules + hooks + user decisions)
- The human-in-the-loop layer
Tool Concurrency
Concurrency-safe tools batch together. Therefore,
- Read-only tools (
Glob,Grep,Read) are safe, so support parallel invocation. - Write tools (
Edit,Write,Bash) are unsafe, so require serial invocation.
// src/services/tools/toolOrchestration.ts
function partitionToolCalls(toolUseMessages, toolUseContext): Batch[] {
return toolUseMessages.reduce((acc, toolUse) => {
const isConcurrencySafe = tool?.isConcurrencySafe(parsedInput.data) ?? false
// Batch consecutive safe tools together
if (isConcurrencySafe && acc[acc.length - 1]?.isConcurrencySafe) {
acc[acc.length - 1].blocks.push(toolUse)
} else {
acc.push({ isConcurrencySafe, blocks: [toolUse] })
}
return acc
}, [])
}
Brief
Tool is the first class citizen in Claude Code. By exposing tools contextually, and providing better guardrails, Claude Code works.
This makes the narrative around “Tool should be abstract and minimal” complicated. And it also opens the gate for more harness engineering around tool design.
Related
Learn From Claude Code: MCP Integration
by a Developer
Apr 2026
by a Developer
agent
ai
Anthropic invented MCP a year ago. Now they're experimenting with skill-ifying it. Here's what the leaked source reveals about the pivot.
Learn From Claude Code: Memory
by a Developer
Apr 2026
by a Developer
agent
ai
Learning Claude Code's memory system by inspecting its leaked source code.
Learn From Claude Code: Permission System
by a Developer
Apr 2026
by a Developer
agent
ai
Learning Claude Code's multi-layer permission architecture by inspecting its leaked source code.
Learn From Claude Code: Query Engine
by a Developer
Apr 2026
by a Developer
agent
ai
The query engine isn't a state machine for academic reasons. It's a recovery coordinator - every continue handles something that went wrong.