---
title: "Tool System — Claude Code Internals — Vlad Arbatov"
description: "Tool interface, registry, execution lifecycle, permissions, deferred tools"
image: "https://arbatov.dev/share-claude.png"
---

# Tool System

## Overview

The Claude Code tool system provides a flexible, permission-aware framework for enabling Claude to interact with the filesystem, execute shell commands, search code, manage files, and perform complex multi-agent orchestration. Each tool is a discrete `Tool` type with input/output schemas, permission checks, lifecycle hooks, and custom rendering.

---

## Core Tool Interface

**File:** `Tool.ts` (792 lines)

The foundational `Tool` type (lines 362-695):

```typescript
export type Tool<
  Input extends AnyObject = AnyObject,
  Output = unknown,
  P extends ToolProgressData = ToolProgressData,
> = {
  name: string
  aliases?: string[]
  searchHint?: string
  
  // Execution
  call(args, context, canUseTool, parentMessage, onProgress?): Promise<ToolResult<Output>>
  description(...): Promise<string>
  inputSchema: Input
  inputJSONSchema?: ToolInputJSONSchema
  outputSchema?: z.ZodType<unknown>
  
  // Safety declarations
  isConcurrencySafe(input): boolean
  isReadOnly(input): boolean
  isDestructive?(input): boolean
  isEnabled(): boolean
  isOpenWorld?(input): boolean
  requiresUserInteraction?(): boolean
  interruptBehavior?(): 'cancel' | 'block'
  shouldDefer?: boolean
  alwaysLoad?: boolean
  isSearchOrReadCommand?(input): { isSearch: boolean; isRead: boolean; isList?: boolean }
  
  // Permissions
  checkPermissions(input, context): Promise<PermissionResult>
  validateInput?(input, context): Promise<ValidationResult>
  preparePermissionMatcher?(input): Promise<(pattern: string) => boolean>
  
  // Result handling
  maxResultSizeChars: number
  mapToolResultToToolResultBlockParam(content, toolUseID): ToolResultBlockParam
  
  // UI rendering
  renderToolUseMessage(input, options): React.ReactNode
  renderToolResultMessage?(content, progressMessages, options): React.ReactNode
  renderToolUseProgressMessage?(progressMessages, options): React.ReactNode
  renderToolUseRejectedMessage?(input, options): React.ReactNode
  
  // Display
  userFacingName(input): string
  getToolUseSummary?(input): string | null
  getActivityDescription?(input): string | null
  
  // MCP
  isMcp?: boolean
  mcpInfo?: { serverName: string; toolName: string }
  
  // Misc
  getPath?(input): string
  toAutoClassifierInput(input): unknown
  extractSearchText?(out): string
}
```

### buildTool() Factory (lines 757-792)

Applies fail-closed defaults:

```typescript
const TOOL_DEFAULTS = {
  isEnabled: () => true,
  isConcurrencySafe: () => false,    // Assume NOT safe
  isReadOnly: () => false,            // Assume writes
  isDestructive: () => false,
  checkPermissions: () => ({ behavior: 'allow', updatedInput: input }),
  toAutoClassifierInput: () => '',
}
```

---

## Tool Registry (tools.ts)

**File:** `tools.ts` (389 lines)

### getAllBaseTools() (lines 193-251)

Exhaustive list of all tools in the environment:

```typescript
export function getAllBaseTools(): Tools {
  return [
    AgentTool, TaskOutputTool, BashTool,
    ...(hasEmbeddedSearchTools() ? [] : [GlobTool, GrepTool]),
    FileReadTool, FileEditTool, FileWriteTool, NotebookEditTool, WebFetchTool,
    // ... 50+ more tools with feature gates
    ...(isToolSearchEnabledOptimistic() ? [ToolSearchTool] : []),
  ]
}
```

### getTools(permissionContext) (lines 271-327)

Filters tools based on deny rules and permission modes:

```typescript
export const getTools = (permissionContext): Tools => {
  if (isEnvTruthy(process.env.CLAUDE_CODE_SIMPLE)) {
    return filterToolsByDenyRules([BashTool, FileReadTool, FileEditTool], permissionContext)
  }
  let allowedTools = filterToolsByDenyRules(getAllBaseTools(), permissionContext)
  if (isReplModeEnabled()) {
    allowedTools = allowedTools.filter(tool => !REPL_ONLY_TOOLS.has(tool.name))
  }
  return allowedTools.filter((_, i) => isEnabled[i])
}
```

### assembleToolPool() (lines 345-367)

Combines built-in + MCP tools with deduplication:

```typescript
export function assembleToolPool(permissionContext, mcpTools): Tools {
  const builtInTools = getTools(permissionContext)
  const allowedMcpTools = filterToolsByDenyRules(mcpTools, permissionContext)
  // Sort each partition separately for prompt cache stability
  return uniqBy(
    [...builtInTools].sort(byName).concat(allowedMcpTools.sort(byName)),
    'name',
  )
}
```

**Cache stability**: Built-in tools form a contiguous prefix. MCP tools appended after. This prevents cache invalidation when MCP servers connect/disconnect.

---

## Key Tool Implementations

### BashTool

- AST-based security analysis (parseForSecurity)
- Streaming output capture with truncation
- Image detection and resizing
- Search/read command classification for UI collapse
- Sandbox mode selection
- Destructive command warnings

**Search/read classification** (lines 95-172):
- `BASH_SEARCH_COMMANDS`: find, grep, rg, ag, ack, locate, which, whereis
- `BASH_READ_COMMANDS`: cat, head, tail, wc, jq, awk, sort, uniq
- `BASH_LIST_COMMANDS`: ls, tree, du
- Piping logic: All parts of pipeline must be search/read for whole command to collapse

### FileReadTool

Reads files with advanced format support:
- Text files with line offsets/limits
- PDFs (up to 20 pages)
- Images (JPEG, PNG, GIF, WebP) with resizing
- Jupyter notebooks with cell parsing

### GlobTool & GrepTool

- **GlobTool**: Finds files by pattern (max 100 results)
- **GrepTool**: Searches file contents with ripgrep, supports context lines, output modes (content/files_with_matches/count), default head_limit of 250 lines

### MCPTool

Template for MCP tools — schema is `z.object({}).passthrough()` (accepts any input). Specialized in mcpClient.ts with real execution and schemas.

### AgentTool

Spawns isolated Claude subagents with worktree isolation, background execution, and model overrides. See `04-agent-coordinator.md`.

### SkillTool

Invokes user-defined skills (prompt commands) with forked execution support. See `07-hooks-skills-permissions.md`.

---

## Tool Execution Lifecycle

```
1. Tool lookup: findToolByName(tools, name)
   ↓
2. Input validation: tool.validateInput(input, context)
   ↓
3. Permission check: tool.checkPermissions(input, context)
   → Returns { behavior: 'allow' | 'ask' | 'deny' }
   ↓
4. Pre-tool hooks: runPreToolUseHooks(tool, input, context)
   ↓
5. Tool execution: tool.call(args, context, canUseTool, parentMessage, onProgress)
   → Always async, reports progress via onProgress callbacks
   ↓
6. Post-tool hooks: runPostToolUseHooks(tool, input, result, context)
   ↓
7. Result formatting: tool.mapToolResultToToolResultBlockParam(result.data, toolUseID)
   ↓
8. Result persistence: if result > maxResultSizeChars, save to disk
   → Directory: ~/.claude/projects/{projectId}/{sessionId}/tool-results/
   → Returns preview + file path to model
```

---

## Permission Model

### Permission Modes

| Mode | Behavior |
|------|----------|
| `default` | Ask user for each tool |
| `plan` | Show plan UI, require approval |
| `acceptEdits` | Auto-approve file edits |
| `bypassPermissions` | Skip all checks (dangerous) |
| `dontAsk` | Deny unknowns, allow known |
| `auto` | Classifier decides (ant-only) |

### Decision Flow

```
1. Permission mode-based allow/deny (plan mode, bypassPermissions)
   ↓
2. Deny rules (blanket denies from settings)
   ↓
3. Allow rules (auto-approve patterns)
   ↓
4. Tool-specific checkPermissions()
   ↓
5. Pre-tool hooks
   ↓
6. Classifier check (if TRANSCRIPT_CLASSIFIER enabled)
   ↓
7. Ask user or auto-deny based on denial tracking
```

### Rule Priority

1. `policySettings` (admin-enforced)
2. `flagSettings` (environment variables)
3. `projectSettings` (`.claude/settings.json`)
4. `userSettings` (`~/.claude/settings.json`)
5. `localSettings` (git-ignored)
6. `cliArg` (command-line flags)
7. `session` (temporary)

### Rule Format

```json
{
  "alwaysAllow": ["Bash(ls)", "Bash(grep *)", "Read(*)"],
  "alwaysDeny": ["Bash(rm -rf /)"],
  "alwaysAsk": ["Bash(git *)", "Edit(*.json)"]
}
```

### Denial Tracking

After 3 denials within 5 minutes → auto-deny further requests. Prevents infinite permission loops.

---

## Deferred Tools & ToolSearchTool

When tool count exceeds ~80, schemas are deferred to reduce initial prompt size.

**Tool flags**:
- `shouldDefer`: Deferred by default
- `alwaysLoad`: Never deferred

### ToolSearchTool

```typescript
// Input
{ query: string, max_results?: number }

// Search strategies:
// 1. Exact match by name
// 2. MCP prefix (mcp__server → all tools from server)
// 3. Keyword search with scoring (name > description > hint)
```

---

## Tool Parameter Schemas

All tools use **Zod v4** with `lazySchema()` for circular dependency breaking:

```typescript
const inputSchema = lazySchema(() =>
  z.strictObject({
    pattern: z.string().describe('The glob pattern'),
    path: z.string().optional(),
  }),
)
```

**Semantic wrappers** for model compliance:
- `semanticNumber()`: Converts strings to numbers
- `semanticBoolean()`: Converts various boolean formats

---

## Concurrency & Safety Properties

| Property | Default | Meaning |
|----------|---------|---------|
| `isConcurrencySafe` | `false` | Can run in parallel with other tools |
| `isReadOnly` | `false` | No side effects |
| `isDestructive` | `false` | Potentially harmful operation |
| `isOpenWorld` | N/A | Unbounded output size |

Tools marked as non-concurrent are serialized in the execution queue.

---

## Result Formatting

**Three rendering paths:**
1. `renderToolUseMessage()` — tool invocation (streaming, partial input)
2. `renderToolResultMessage()` — completed result with rich formatting
3. `renderToolUseProgressMessage()` — real-time progress updates
4. `renderToolUseRejectedMessage()` — permission denied display

**Large result persistence:**
- Threshold: `maxResultSizeChars` per tool
- Storage: `~/.claude/projects/{project}/{session}/tool-results/{toolUseId}.json`
- Preview: first 2000 bytes returned inline
- Wrapped in `<persisted-output>` tags
