Skip to content

A05: Tool Calling Internals ​

Related chapters: Ch1 Installation & First Session, Ch5 Working with Files & Shell

Every time Claude Code reads a file, edits code, runs a shell command, or searches your codebase, it is making a tool call. Tool calling is the mechanism that transforms Claude from a text generator into a capable software engineering agent. This appendix dives into the internals: how Claude selects tools, how parameters are structured, how results flow back, how permissions gate actions, and how you can extend the system with custom tools via MCP.

How Tool Calling Works: The Protocol ​

The Conversation Structure ​

Claude Code communicates with the Claude API using a structured message format. Each message in the conversation has a role and content:

Conversation messages (simplified):

{ role: "system",    content: "You are Claude Code..." }
{ role: "user",      content: "Fix the auth bug" }
{ role: "assistant", content: "I'll investigate the auth module.",
                     tool_use: { name: "Read", input: { file_path: "src/auth/middleware.ts" } } }
{ role: "tool",      content: "[file contents...]",
                     tool_use_id: "toolu_01abc..." }
{ role: "assistant", content: "I found the issue on line 24.",
                     tool_use: { name: "Edit", input: { ... } } }
{ role: "tool",      content: "File edited successfully.",
                     tool_use_id: "toolu_01def..." }
{ role: "assistant", content: "The bug was a unit mismatch..." }

The key insight: tool calls are part of Claude's response, not separate from it. When Claude generates a response, it can include both text and one or more tool calls. The harness executes the tool calls and feeds results back as tool role messages. Then Claude continues generating.

Tool Definitions: The Menu of Available Actions ​

Before the conversation begins, the harness provides Claude with a list of tool definitions. Each definition includes:

  • Name: The tool identifier (e.g., Read, Edit, Bash)
  • Description: What the tool does (natural language)
  • Parameters: A JSON Schema defining the inputs
  • Required parameters: Which inputs are mandatory
json
{
  "name": "Read",
  "description": "Reads a file from the local filesystem. You can access any file directly by using this tool.",
  "parameters": {
    "type": "object",
    "properties": {
      "file_path": {
        "type": "string",
        "description": "The absolute path to the file to read"
      },
      "offset": {
        "type": "integer",
        "description": "The line number to start reading from",
        "minimum": 0
      },
      "limit": {
        "type": "integer",
        "description": "The number of lines to read",
        "exclusiveMinimum": 0
      }
    },
    "required": ["file_path"]
  }
}

These definitions serve two purposes:

  1. They tell Claude what tools exist and when to use each one
  2. They tell the harness how to validate Claude's tool call parameters before execution

How Claude Selects Tools ​

The Decision Process ​

When Claude receives a prompt, it must decide: should I respond with text, or should I call a tool? And if a tool, which one?

This decision is not made by a separate "tool routing" module. It emerges from the same next-token prediction that generates all of Claude's output. The tool definitions are part of the context, and Claude's training has taught it to generate tool call tokens when appropriate.

The process works roughly like this:

Claude's internal decision (conceptual):

Context includes:
  - System prompt saying "you have these tools available"
  - Tool definitions with descriptions and schemas
  - User says "Read the auth middleware"

Claude's attention mechanism weighs:
  - "Read" in the user prompt matches the "Read" tool name
  - "auth middleware" suggests a file path is needed
  - The Read tool's description says "reads a file"
  - Previous turns show examples of Read tool usage

Claude generates:
  → tool_use token (instead of regular text)
  → name: "Read"
  → input: { "file_path": "src/auth/middleware.ts" }

What Influences Tool Selection ​

Several factors influence which tool Claude chooses:

1. Tool description quality

The description is the most important factor. Claude reads the description to understand when a tool is appropriate. Compare:

# Weak description — Claude may not know when to use this
"Runs a command"

# Strong description — Claude understands exactly when to use this
"Executes a given bash command and returns its output. Use for 
running tests, installing packages, checking git status, or any 
shell operation."

2. Parameter schema specificity

Well-defined parameter schemas guide Claude toward correct usage:

json
// Vague schema — Claude may pass wrong types
{
  "path": { "type": "string" }
}

// Specific schema — Claude knows exactly what to provide
{
  "file_path": {
    "type": "string",
    "description": "The absolute path to the file to read (must be absolute, not relative)"
  }
}

3. Conversation context

Claude learns from previous tool calls in the conversation. If it used Read to examine a file and now needs to modify it, it has learned the file's path and format, making the subsequent Edit tool call more accurate.

4. System prompt instructions

The system prompt includes guidance on tool preferences. For example, Claude Code's system prompt instructs Claude to prefer Edit over Write for modifying existing files, and to prefer Read over Bash("cat ...") for reading files. These instructions shape tool selection.

The Tool Selection Decision Tree ​

Here is the conceptual decision tree Claude follows for common Claude Code tasks:

Need information about a file?
├── Know the exact path?
│   └── Read (with file_path)
├── Know the filename but not the path?
│   └── Bash("find . -name 'filename'") → then Read
├── Know a pattern in the file content?
│   └── Bash("grep -r 'pattern' src/") → then Read
└── Need to understand directory structure?
    └── Bash("ls -la src/") or Bash("find . -type f -name '*.ts'")

Need to modify a file?
├── File exists?
│   ├── Small, targeted change?
│   │   └── Edit (with old_string/new_string)
│   └── Complete rewrite?
│       └── Write (after Read)
└── New file?
    └── Write

Need to execute something?
├── Shell command?
│   └── Bash
├── Need real-time output?
│   └── Bash (with timeout)
└── Long-running process?
    └── Bash (with run_in_background)

Tool Result Formatting ​

How Results Enter the Context ​

When a tool executes, its result is formatted and added to the conversation as a tool role message. The formatting varies by tool:

Read tool results include line numbers:

1	import { Router } from 'express';
2	import { validateToken } from './auth';
3	
4	const router = Router();
5	
6	router.get('/api/users', validateToken, (req, res) => {
7	  // ...
8	});

Bash tool results include stdout and stderr:

$ npm test

  PASS  src/auth/__tests__/middleware.test.ts
  ● Console

    console.log
      Token validated successfully

  Tests:       3 passed, 3 total
  Time:        1.234 s

Edit tool results confirm the change:

File edited successfully.

How Claude Parses Results ​

Claude processes tool results the same way it processes any other context: through the attention mechanism. It does not have special "result parsing" logic. This means:

  1. Well-formatted results are easier for Claude to use. Line numbers in Read output help Claude reference specific lines. Structured test output helps Claude identify which tests failed.

  2. Verbose results waste context. A 500-line test output where only 3 lines show failures wastes ~2,000 tokens on irrelevant information. This is why controlling output verbosity matters (see A02 Context Engineering).

  3. Claude can misinterpret ambiguous results. If a command produces output that could mean success or failure, Claude may misread it. Clear success/failure signals (exit codes, explicit PASS/FAIL messages) help.

Error Handling in Tool Calls ​

What Happens When a Tool Fails ​

Tool calls can fail in several ways, each handled differently:

1. Validation error (before execution)

The harness validates Claude's parameters against the JSON Schema before executing the tool. If validation fails, the error is returned immediately:

Claude: Edit({ file_path: "src/auth.ts", old_string: "", new_string: "..." })
Harness: Error — old_string cannot be empty

Claude: "I need to provide the text to replace. Let me read the file first 
to find the exact text."

2. Execution error (during execution)

The tool runs but fails:

Claude: Bash("npm test")
Result: Error: ENOENT - package.json not found

Claude: "We're not in the right directory. Let me check the 
project structure first."
Claude: Bash("ls")

3. Timeout error

The tool takes too long:

Claude: Bash("npm install", timeout: 30000)
Result: Error: Command timed out after 30 seconds

Claude: "The install is taking too long. Let me try with a longer 
timeout or check if there's a network issue."

4. Permission denied

The permission system blocks the tool call:

Claude: Bash("rm -rf /important/data")
Harness: [Permission denied — user must approve]
User: [Denies]
Result: Error: Permission denied by user

Claude: "I understand. That was too broad. Let me use a more 
targeted approach."

Error Recovery Patterns ​

Claude's error recovery follows predictable patterns:

Error TypeClaude's Typical Recovery
File not foundSearch for the correct file path
Edit target not foundRe-read the file to get current content
Command failedRead the error message, adjust the command
Permission deniedAsk the user or try a less privileged approach
TimeoutRetry with longer timeout or break the task into smaller parts
Syntax error in outputRe-run with different formatting flags

Good error handling is one of the areas where model size matters most. Larger models (Opus) are better at diagnosing errors from context clues and trying meaningfully different approaches, rather than retrying the same failing command.

The Permission System ​

How Permissions Work ​

Claude Code's permission system is a safety layer between Claude's tool call decisions and actual execution. Every tool call passes through the permission system before execution:

Permission evaluation flow:

Claude generates tool call
        │
        ▼
┌──────────────────┐
│ Is this tool in   │──── Yes ──── Execute immediately
│ the allowlist?    │
└───────┬──────────┘
        │ No
        ▼
┌──────────────────┐
│ Is this tool in   │──── Yes ──── Block and return error
│ the denylist?     │
└───────┬──────────┘
        │ No
        ▼
┌──────────────────┐
│ Prompt user for   │──── Allow ──── Execute
│ permission        │──── Deny  ──── Return error
└──────────────────┘

Permission Configuration Layers ​

Permissions are configured at multiple levels, with more specific levels overriding broader ones:

Permission resolution order:

1. CLI flags (--allowedTools, --disallowedTools)
   Most specific — overrides everything

2. Project settings (.claude/settings.json)
   Project-specific permissions

3. User settings (~/.claude/settings.json)
   Global personal permissions

4. Default permissions
   Built-in defaults for each tool

Tool Risk Classification ​

Tools are implicitly classified by their risk level:

Read-only tools (low risk):

  • Read -- reads file contents
  • Glob -- lists files matching patterns
  • Grep -- searches file contents

These are typically auto-allowed because they cannot modify your system.

Write tools (medium risk):

  • Edit -- modifies existing files
  • Write -- creates or overwrites files

These require permission by default because they change your code.

Execution tools (high risk):

  • Bash -- runs arbitrary shell commands

Bash is the highest risk tool because it can do anything: delete files, install packages, make network requests, modify system configuration. Permission rules for Bash are typically the most granular:

json
{
  "permissions": {
    "allow": [
      "Bash(npm test)",
      "Bash(npm run lint)",
      "Bash(git status)",
      "Bash(git diff*)"
    ]
  }
}

This allows specific safe commands while still prompting for approval on anything else.

Tool Call Batching and Parallelism ​

Sequential vs Parallel Tool Calls ​

Claude can generate multiple tool calls in a single response. When tool calls are independent, the harness can execute them in parallel:

Sequential execution (dependent calls):
  Claude: Read("src/auth/middleware.ts")
  Result: [file contents]
  Claude: Edit("src/auth/middleware.ts", { old_string: "...", new_string: "..." })
  Result: File edited

The Edit depends on the Read result, so they must be sequential.

Parallel execution (independent calls):
  Claude: Read("src/auth/middleware.ts")
          Read("src/auth/types.ts")
          Read("src/auth/utils.ts")
  Results: [all three files read simultaneously]

The Reads are independent, so they execute in parallel.

When Claude Batches Tool Calls ​

Claude tends to batch tool calls when:

  • Reading multiple files that it knows the paths to
  • Running independent commands (e.g., checking git status and running linter)
  • Gathering information from multiple sources before making a decision

Claude tends to execute sequentially when:

  • Each tool call depends on the previous result
  • It is uncertain about the correct path and needs to explore
  • It is in a trial-and-error loop (edit, test, fix)

Optimizing for Parallelism ​

You can encourage parallel tool calls by:

# Encourages parallel reads
You: "Read these three files and compare their approaches to error 
handling: src/auth/errors.ts, src/api/errors.ts, src/db/errors.ts"

# Encourages sequential (Claude must read before analyzing)
You: "Find all error handling files, then analyze their patterns"

The first prompt gives Claude all three paths upfront, enabling parallel reads. The second requires a search step first, forcing sequential execution.

Custom Tools via MCP ​

How MCP Tools Are Discovered ​

The Model Context Protocol (MCP) allows you to extend Claude Code with custom tools. When Claude Code starts, it connects to configured MCP servers and discovers their available tools:

MCP tool discovery:

1. Claude Code reads MCP server configuration from settings
2. Connects to each configured MCP server
3. Calls the server's `tools/list` endpoint
4. Receives tool definitions (name, description, schema)
5. Adds these definitions to the tool list alongside built-in tools
6. Claude now "sees" custom tools in its context and can use them

Custom Tool Definitions ​

A well-designed MCP tool definition looks like this:

json
{
  "name": "database_query",
  "description": "Execute a read-only SQL query against the development database. Returns results as a JSON array of objects. Use this when you need to inspect data, verify migrations, or understand the current database state. Only SELECT queries are allowed.",
  "inputSchema": {
    "type": "object",
    "properties": {
      "query": {
        "type": "string",
        "description": "The SQL SELECT query to execute. Must start with SELECT."
      },
      "limit": {
        "type": "integer",
        "description": "Maximum number of rows to return (default: 100, max: 1000)",
        "default": 100,
        "maximum": 1000
      }
    },
    "required": ["query"]
  }
}

Key design principles for custom tools:

  1. Descriptive names: database_query is better than db_q. Claude uses the name as a signal.
  2. Detailed descriptions: Explain WHEN to use the tool, not just what it does. The description is Claude's primary guide for tool selection.
  3. Constrained schemas: Use minimum, maximum, enum, and pattern to prevent misuse. A tool that only accepts SELECT queries is safer than one that accepts any SQL.
  4. Helpful parameter descriptions: Each parameter description should explain the expected format and any constraints.

How Claude Treats Custom Tools ​

Claude treats MCP tools identically to built-in tools. It sees them in the same tool list, reads their descriptions, and generates tool calls using the same mechanism. There is no special "custom tool mode."

This means:

  • Good descriptions make custom tools discoverable: If your tool description clearly explains when to use it, Claude will select it appropriately.
  • Bad descriptions cause confusion: If your custom tool's description overlaps with a built-in tool, Claude may choose the wrong one.
  • Schema quality affects parameter accuracy: Loose schemas lead to more parameter errors.

Hooks: Extending the Tool-Call Pipeline ​

What Hooks Are ​

Hooks allow you to inject custom behavior at specific points in the tool-call pipeline. They run as shell commands triggered by tool-call events:

Hook integration points:

User prompt arrives
    │
    ▼
Claude generates tool call
    │
    ▼
┌──────────────────┐
│ PreToolCall hook  │ ← Runs BEFORE the tool executes
└───────┬──────────┘   Can modify parameters or block the call
        │
        ▼
    Tool executes
        │
        ▼
┌──────────────────┐
│ PostToolCall hook │ ← Runs AFTER the tool executes
└───────┬──────────┘   Can modify results or trigger side effects
        │
        ▼
Result returned to Claude

Hook Configuration ​

Hooks are configured in settings.json:

json
{
  "hooks": {
    "preToolCall": [
      {
        "matcher": "Edit",
        "command": "node .claude/hooks/lint-before-edit.js"
      }
    ],
    "postToolCall": [
      {
        "matcher": "Bash",
        "command": "node .claude/hooks/log-commands.js"
      }
    ]
  }
}

Practical Hook Use Cases ​

Use CaseHook TypeMatcherCommand
Auto-format after editpostToolCallEditnpx prettier --write $TOOL_INPUT_FILE_PATH
Log all commandspostToolCallBashecho "$(date): $TOOL_INPUT_COMMAND" >> .claude/command-log.txt
Block protected filespreToolCallEditnode .claude/hooks/protect-files.js
Lint before commitpreToolCallBashnode .claude/hooks/check-lint.js

Hooks extend the tool-call pipeline without modifying Claude Code itself. They are particularly useful for team workflows where you want to enforce conventions (formatting, linting) or track agent behavior (command logging, change auditing).

Putting It All Together: A Tool Call's Lifecycle ​

Here is the complete lifecycle of a single tool call, from Claude's decision to the result entering context:

1. Context assembly
   System prompt + CLAUDE.md + history + user prompt
   → sent to Claude API

2. Token generation
   Claude generates tokens, including a tool_use block
   → { name: "Bash", input: { command: "npm test" } }

3. Schema validation
   Harness validates input against Bash tool's JSON Schema
   → Is "command" a string? Yes → valid

4. Permission check
   Harness checks permission rules
   → Is "Bash(npm test)" in the allowlist? Yes → proceed

5. Pre-hook execution (if configured)
   Hook runs before the tool executes
   → Pre-hook logs the command, returns "proceed"

6. Tool execution
   Harness runs: npm test
   → stdout: "Tests: 5 passed", exit code: 0

7. Post-hook execution (if configured)
   Hook runs after execution
   → Post-hook logs success

8. Result formatting
   Harness formats the output for the conversation
   → { role: "tool", content: "Tests: 5 passed\n...", tool_use_id: "toolu_..." }

9. Context update
   Result is appended to conversation history
   → Context grows by ~200 tokens

10. Next turn
    Updated context is sent back to Claude API
    Claude generates next response (text or more tool calls)

Key Takeaways ​

  1. Tool calls are generated through next-token prediction, not a separate routing system. Claude "decides" to use a tool the same way it decides to write any text -- by predicting the most useful next token.
  2. Tool descriptions are the primary factor in tool selection. Write detailed, specific descriptions that explain WHEN to use the tool, not just what it does.
  3. JSON Schema constrains tool usage. Well-defined schemas with types, ranges, and descriptions reduce parameter errors.
  4. The permission system is a safety layer between decision and execution. Configure it to auto-allow safe operations and gate dangerous ones.
  5. Error handling is the model's responsibility, but you can help by encouraging re-read-before-retry patterns and intervening after repeated failures.
  6. Custom tools (MCP) are first-class citizens. Claude treats them identically to built-in tools. Invest in good descriptions and schemas.
  7. Hooks extend the pipeline without modifying Claude Code. Use them for formatting, logging, and enforcement.

See also: A04 Agent Architecture Patterns for how tool calling fits into the larger agentic loop, and A06 MCP Protocol Deep Dive for the full protocol specification.

Released under MIT License