Claude Code のツールアーキテクチャ

@spandan_madan
英語4 週間前 · 2026年6月17日
552K
416
115
86
479

TL;DR

本記事では、Claude Code のツールアーキテクチャを技術的に深く掘り下げます。複雑な AI エージェントの動作を可能にする 7 段階のディスパッチパイプライン、並行処理スケジューラー、およびフェイルクローズ型の権限システムについて詳しく解説します。

以下が、ご指定のルールに従って英日翻訳したものです。マークダウン構造、コードセグメント、ペイロードブロック、およびすべての書式を維持しています。コード内のコメントも適切に翻訳し、日本語として自然な表現になるよう調整しました。


Claude Code がツールを発見、ディスパッチ、実行する仕組みの詳細

はじめに

以前の記事では、Anthropic Harness のメモリシステムを流出したソースコードからリバースエンジニアリングしました。メモリは驚くほどシンプルでした — マークダウンファイル、フロントマター、プロンプトエンジニアリングです。ツールシステムはその正反対です。これはコードベース全体で最もエンジニアリングされたサブシステムです。43 以上のツール、ストリーミング実行パイプライン、レイヤー化された権限システム、フックフレームワーク、並行処理スケジューラ — これらすべてが連携して、ステートレスな言語モデルをファイルの読み取り、シェルコマンドの実行、Web 検索、サブエージェントの起動ができるものに変えています。

この記事では、ツールのライフサイクルを、ツールがどのように定義されるか、モデルのツールコールがどのようにディスパッチされるか、結果がどのように会話に戻されるかまで、順を追って説明します。大まかに言えば、システムは 4 つのレイヤーで構成されています。すべてのツールが実装するツールインターフェース、ツールプールを組み立てるレジストリ、各コールを検証、権限チェック、実行するディスパッチパイプライン、そして何を並行実行するかを決定する並行処理スケジューラです。

アーキテクチャの概要

Spandan Madan - inline image

ツールインターフェース

Claude Code のすべてのツールは、Tool.ts で定義された同じインターフェースを実装しています。この型は Input(Zod スキーマ)、Output(結果の型)、P(進捗データ)の 3 つのパラメーターでジェネリックになっています。実際には、ツールは約 30 のメソッドを持つオブジェクトですが、システムを理解する上で重要なのはそのうちのわずかです。

コアの形状:

text
1type Tool<Input, Output, P> = {
2 name: string
3 inputSchema: ZodType // 入力検証用の Zod スキーマ
4 call(input, context, canUseTool,
5 parentMessage, onProgress): Promise<ToolResult>
6
7 // 動作宣言
8 isConcurrencySafe(input): boolean // 並行実行可能?
9 isReadOnly(input): boolean // 読み取り専用?
10 isDestructive(input): boolean // 破壊的操作?
11
12 // 権限と検証
13 checkPermissions(input, context): Promise<PermissionResult>
14 validateInput(input, context): Promise<ValidationResult>
15
16 // API 統合
17 description(input, options): Promise<string>
18 prompt(options): Promise<string> // このツール用のシステムプロンプトテキスト
19 mapToolResultToToolResultBlockParam(result, toolUseId): ToolResultBlockParam
20
21 // UI レンダリング (React)
22 renderToolUseMessage(input, options): ReactNode
23 renderToolResultMessage(content, ...): ReactNode
24}

どのツールもこれをゼロから実装するわけではありません。buildTool() というファクトリー関数が安全なデフォルトを提供します:

Spandan Madan - inline image

デフォルトは意図的に保守的になっています。並行処理の安全性を宣言し忘れたツール作成者は、直列実行になります。権限チェックを実装し忘れたツール作成者は、デフォルトの権限フローを使用します。システムは、安全側に倒して失敗します。

ToolResult 型も注目に値します:

text
1type ToolResult<T> = {
2 data: T // 実際の出力
3 newMessages?: Message[] // オプションのフォローアップメッセージ
4 contextModifier?: (ctx) => ToolUseContext // 次のツール用にコンテキストを変更
5 mcpMeta?: { ... } // MCP プロトコルメタデータ
6}

contextModifier は重要です。これにより、ツールは同じターン内の後続のツールの実行コンテキストを変更できます。これが EnterWorktree のようなツールが、後続のすべての処理のワーキングディレクトリを変更する方法です。重要なのは、コンテキストモディファイアは並行処理安全でないツールにのみ許可されることです。ツールが並行実行される場合、共有状態を変更することはできません。

ツールレジストリ

すべてのツールは、tools.ts の 1 つの関数 getAllBaseTools() に登録されています。これはフラットな配列を返します。一部のツールは常に利用可能で、その他は機能フラグ、環境変数、またはプラットフォームチェックの背後にゲートされています。

常に利用可能(16 ツール)

Spandan Madan - inline image

機能フラグでゲートされたツール(約 27 ツール)

残りのツールは条件付きで含まれます。一部は環境変数(Anthropic 内部ツール用の USER_TYPE=antconfigtungsten など)、一部は Statsig 経由の機能フラグ(web_browsersleepmonitor)、一部はプラットフォーム固有(Windows の powershell)、一部は複合条件でゲートされています — repl ツールには USER_TYPE=ant と REPL 機能フラグの両方が必要です。

機能フラグでゲートされたツールの完全リスト

Ant のみ: config、tungsten、suggest_background_pr、repl(REPL フラグも必要)

機能フラグ: web_browser、web_search、sleep、monitor、overflow_test、ctx_inspect、terminal_capture、list_peers、workflow、snip

エージェントトリガー: cron_create、cron_delete、cron_list、remote_trigger

Kairos(プロアクティブエージェント): sleep、send_user_file、push_notification、subscribe_pr

マルチエージェントスウォーム: team_create、team_delete、send_message

Todo v2: task_create、task_get、task_update、task_list

環境: lsp(ENABLE_LSP_TOOL)、enter_worktree / exit_worktree(worktree モード)、powershell(Windows)

ツール発見: tool_search(ツールプールが大きい場合)

テストのみ: testing_permission(NODE_ENV=test)

MCP ツール

組み込みツールに加えて、Claude Code は Model Context Protocol (MCP) サーバーをサポートしています。これは、標準化されたプロトコルを介して独自のツールを公開する外部プロセスです。MCP ツールは、接続されたサーバーから実行時に動的に登録され、同じ Tool インターフェースでラップされます。ディスパッチパイプラインから見ると、MCP ツールは組み込みツールと区別がつきません。

各 MCP ツールは、その起点サーバーに関するメタデータ(mcpInfo: { serverName, toolName })を持ちます。これは権限ルール、エラーハンドリング、認証に使用されます。MCP ツールが認証エラーで失敗すると、システムは自動的にサーバーのステータスを needs-auth に更新し、ユーザーに問題を知らせます。

ツールプールの組み立て

3 つの関数が最終的なツールセットを組み立てます:

  1. getAllBaseTools() — 機能ゲートが適用された 43 以上の組み込みツールの生のリストを返します
  2. getTools(permissionContext) — 拒否ルールと isEnabled() でフィルタリングします
  3. assembleToolPool(permissionContext, mcpTools) — 組み込みツールと MCP ツールをマージします

assembleToolPool() のマージ戦略は意図的です:

組み込みツールが最初に来るため、名前の競合が発生した場合は組み込みツールが優先されます。各パーティション内のアルファベット順ソートにより、セッション間で順序が安定します。これはプロンプトキャッシュにとって重要です。ツール配列は API リクエストの一部であり、それを並べ替えるとキャッシュが無効になるからです。

text
1// 各パーティションをアルファベット順にソートし、連結して重複を除去
2const byName = (a, b) => a.name.localeCompare(b.name)
3return uniqBy(
4 [...builtInTools].sort(byName).concat(allowedMcpTools.sort(byName)),
5 'name',
6)

API シリアライゼーション

ツールが Claude API に到達する前に、toolToAPISchema() が各ツールの Zod スキーマを Anthropic API の JSON Schema 形式に変換します。

ディスパッチパイプライン

Claude が応答すると、そのメッセージには tool_use ブロック(ツールを呼び出す構造化されたリクエスト)が含まれている場合があります。ディスパッチパイプラインは、これらのブロックを 7 つのフェーズで処理します。すべてのツールコールは、すべてのフェーズを順に通過します。

フェーズ 1: 抽出

メインのクエリループ(query.ts)で、tool_use ブロックがアシスタントメッセージからフィルタリングされます:

text
1const msgToolUseBlocks = message.message.content.filter(
2 content => content.type === 'tool_use',
3) as ToolUseBlock[]

各ブロックには nameinput オブジェクト、および一意の id があります。id は重要です。ツールの結果は、API に送り返すときに同じ id を参照する必要があります。そうしないと、会話が壊れます。

フェーズ 2: 入力検証

ツールの Zod スキーマは、safeParse() を使用して生の入力を検証します。これは、有効なデータまたは構造化エラーを返すスローしないバリアントです。検証が失敗した場合、モデルはスキーマヒントを含むフォーマットされたエラーメッセージを受け取り、そのツールの実行は停止します。無効な入力ではコードは実行されません。

text
1const parsedInput = tool.inputSchema.safeParse(input)
2if (!parsedInput.success) {
3 let errorContent = formatZodValidationError(tool.name, parsedInput.error)
4 // モデルにエラーを返し、実行をスキップ
5}

Zod 検証の後、一部のツールはスキーマでは表現できない意味的検証のために 2 番目の validateInput() チェックを実行します。たとえば、ファイルパスが相対パスではなく絶対パスであることを確認するなどです。

フェーズ 3: ツール前フック

権限チェックの前に、ユーザー設定のフックが実行されます。これらはツールの呼び出し時に起動する外部シェルコマンドまたはスクリプトです。ツール前フックは以下を実行できます:

  • 許可: ツールコールを許可し、インタラクティブな権限プロンプトをバイパスします
  • 拒否: ツールコールを完全に拒否します
  • 入力の変更: 実行前に入力を変更します
  • 実行のブロック: エラーメッセージで実行をブロックします
  • 追加コンテキストの提供: ユーザーに追加のコンテキストを提供します

重要な不変条件: フックの allow は、設定からの拒否ルールをバイパスしません。ソースコードにはこれについて明示的なコメントがあります。「フックの 'allow' は settings.json の deny/ask ルールをバイパスしません。」意図は、フックはドアを開けることはできても、ロックを上書きすることはできないということです。

フェーズ 4: 権限チェック

権限システムはパイプラインの中で最も複雑な部分です。複数のレイヤーを順に解決します:

  1. 拒否ルール — 最初にチェックされます。拒否ルールが一致すると、実行は即座に停止します。拒否ルールは最終的なものであり、他のどのレイヤーでもオーバーライドできません。
  2. 確認ルール — 一致すると、ユーザーに承認を求めます(Bash のサンドボックス自動許可が適用される場合を除く)。
  3. ツール固有の権限 — ツール独自の checkPermissions() メソッドが実行されます。たとえば、BashTool はコマンドを解析してサブコマンドレベルのルールをチェックします。
  4. 安全チェック — 機密パス(.git/.claude/、シェル設定)に対するハードコードされた保護。これらはバイパス耐性があります。完全バイパスモードでも、インタラクティブな承認が必要です。
  5. 権限モード — ユーザーの設定モードがデフォルトの動作を決定します。
  6. 許可ルール — 最後にチェックされます。許可ルールが一致し、拒否ルールや確認ルールがトリガーされなかった場合、ツールは続行します。

権限モード

default — 「確認」の決定については常にユーザーにプロンプトを表示します。

acceptEdits — 安全なファイル操作(読み取り、編集)を自動許可し、その他はプロンプトを表示します。

bypassPermissions — 拒否ルールと安全チェックを除くすべてを自動許可します。

plan — 最初に計画を承認し、その後前のモードに従って実行します。

auto — AI 分類器を使用して、許可するかプロンプトを表示するかを決定します。

dontAsk — すべての「確認」の決定を「拒否」に変換します。プロンプトは表示せず、単に拒否します。

権限ルールは複数のソースから取得され、優先順位に従って解決されます: policySettingslocalSettingsprojectSettingsuserSettingsflagSettingscliArgcommandsession。これにより、組織ポリシーがユーザー設定をオーバーライドし、CLI 引数がその両方をオーバーライドできます。

フェーズ 5: 実行

権限が付与されると、ツールの call() メソッドが呼び出されます:

text
1const result = await tool.call(
2 callInput,
3 { ...toolUseContext, toolUseId: toolUseID },
4 canUseTool,
5 assistantMessage,
6 progress => onToolProgress({ toolUseID: progress.toolUseID, data: progress.data })
7)

5 つの引数: 検証済みの入力、実行コンテキスト(ワーキングディレクトリ、アボートコントローラー、アプリ状態)、権限コールバック(実行途中で追加の権限を要求する必要があるツール用)、親アシスタントメッセージ、リアルタイム更新用の進捗コールバック。期間はグローバルに追跡されます。

微妙な詳細: call() に渡される入力は、フックと権限が参照したバックフィルされたバージョンではなく、モデルの元の入力です。これにより、トランスクリプトの一貫性が保たれます — 会話に記録されたツールコールは、モデルが生成したものと完全に一致します。

フェーズ 6: ツール後フック

実行後、ツール後フックが起動します。これらは MCP ツールの出力を変更したり、追加のコンテキストを提供したり、会話の継続をブロックしたりできます。また、エラー時にのみ起動する別の PostToolUseFailure フックもあり、外部システムが障害をログに記録したり、修復を提案したりする機会を提供します。

フェーズ 7: 結果マッピング

各ツールは mapToolResultToToolResultBlockParam() を実装し、その出力を Anthropic API の ToolResultBlockParam 形式(tool_use_id 参照と文字列または構造化コンテンツを持つ tool_result ブロック)に変換します。

結果がサイズしきい値を超える場合、sessionDir/tool-results/{toolUseId}.txt にディスクに永続化され、代わりにファイル参照付きのプレビューが API に送信されます。これにより、大きな出力(10,000 行のファイル読み取り、長大なコマンド出力)が会話コンテキストを肥大化するのを防ぎます。

並行処理スケジューラ

モデルが 1 つのメッセージで複数のツールコールを発行しても、それらがすべて同時に実行されるわけではありません。スケジューラは、並行処理の安全性に基づいてそれらをバッチに分割します。

アルゴリズムはシンプルです。ツールコールを順に処理します。それぞれについて、isConcurrencySafe(input) をチェックします。安全で、前のバッチも安全だった場合は、そのバッチに追加します。そうでなければ、新しいバッチを開始します。

text
1// toolOrchestration.ts から簡略化
2for (const toolUse of toolUseMessages) {
3 const isSafe = tool.isConcurrencySafe(parsedInput)
4 if (isSafe && lastBatch.isConcurrencySafe) {
5 lastBatch.blocks.push(toolUse) // 並行バッチにマージ
6 } else {
7 batches.push({ isConcurrencySafe: isSafe, blocks: [toolUse] })
8 }
9}

安全なバッチは並行実行されます(上限 10、CLAUDE_CODE_MAX_TOOL_USE_CONCURRENCY で設定可能)。安全でないバッチは 1 つのツールずつ直列実行されます。コンテキストモディファイアはバッチ間でのみ適用され、バッチ内では適用されません。

実際には、これは「これらの 5 つのファイルを読んで」のようなメッセージは 1 つの並行バッチを生成し、「このファイルを読んでから編集して」のようなメッセージは 2 つの直列バッチを生成することを意味します。モデルは 1 回のターンで両方のパターンをトリガーすることもできます。連続する読み取り専用コールはバッチ化され、最初の書き込みがバッチを分割します。

ストリーミングエグゼキュータ

2 つ目の実行パスとして StreamingToolExecutor があります。ストリーミングが有効な場合、ツールはモデルが応答を生成している間に実行を開始します。ストリーム内の各 tool_use ブロックが完了すると、完全な応答を待たずにすぐに実行キューに入れられます。

ストリーミングエグゼキュータは同じ並行処理ルールを使用しますが、1 つの動作を追加します: Bash エラーのカスケードです。並行実行中の兄弟ツールがある状態で Bash コマンドが失敗した場合、エグゼキュータはすべての兄弟をアボートします。その根拠は、Bash コマンドの失敗は他のツールが動作しているコンテキストを無効にする可能性が高く、それらのツールを続行すると時間の無駄になり、混乱を招くエラーを引き起こす可能性があるというものです。

text
1if (isErrorResult && tool.block.name === BASH_TOOL_NAME) {
2 this.hasErrored = true
3 this.siblingAbortController.abort('sibling_error')
4}

具体的な例

これを具体化するために、モデルがファイルの読み取りを決定した場合の流れを追ってみましょう。モデルが以下を出力します:

text
1{
2 "type": "tool_use",
3 "id": "toolu_01XYZ",
4 "name": "read",
5 "input": { "file_path": "/src/index.ts" }
6}

抽出: query.ts がこれをアシスタントメッセージのコンテンツからフィルタリングします。

  1. ツール検索: findToolByName(tools, "read") が FileReadTool を見つけます。
  2. 入力検証: Zod が { file_path: "/src/index.ts" }z.object({ file_path: z.string(), offset: z.number().optional(), limit: z.number().optional(), pages: z.string().optional() }) に対して解析します。パスします。
  3. ツール前フック: ユーザー設定のフックが起動します。入力を変更するものはありません。
  4. 権限チェック: FileReadTool の checkPermissions()checkReadPermissionForTool() を呼び出します。読み取りツールは通常、ほとんどの権限モードで許可されます。
  5. 実行: FileReadTool.call() がファイルを読み取り、行番号を適用し(cat -n 形式)、PDF/画像/ノートブックを特別なケースとして処理します。
  6. 結果マッピング: ファイルの内容が "toolu_01XYZ" を参照する tool_result ブロックになります。
  7. 返却: 結果はユーザーメッセージとして会話に追加され、次の API 呼び出しで送信されます。

FileReadTool が isConcurrencySafe: () => true および isReadOnly: () => true を宣言しているため、モデルが同じメッセージで 5 つの読み取り呼び出しを出力した場合、5 つすべてが並行実行されます。

まとめ

ツールシステムは Claude Code の実行のバックボーンです。モデルの意図を構造化された tool_use ブロックとして表現し、検証、権限、並行処理制御を各ステップで適用して、実際のマシン上のアクションに変換します。

設計はレイヤー化されています。保守的な buildTool() ファクトリーが安全なデフォルトを保証し、機能ゲートされたレジストリが利用可能なものを制御し、7 フェーズのディスパッチパイプラインがすべての呼び出しを検証および権限チェックし、並行処理スケジューラが正確性を維持しながら並列性を最大化します。ストリーミングエグゼキュータはその上にパフォーマンス最適化を追加します — モデルが考え終わる前にツールの実行が開始されます。

メモリシステム(5 つのパス、マークダウンファイルのディレクトリ、プロンプトエンジニアリング)と比較すると、ツールシステムは本格的なランタイムです。それはファイルキャビネットとオペレーティングシステムの違いです。

興味深い点

スケジューラとしてのモデル

並行処理スケジューラはリアクティブです。モデルが出力するものをそのままバッチ化します。しかし、実際のスケジューラはモデル自身です。システムプロンプトは、「独立したツールコールはすべて並行して行い」、「依存するコマンドは && で連結して 1 つの Bash 呼び出しを使用する」ように指示します。ランタイムはこれを信頼します。モデルが 5 つの読み取りの後に 1 つの書き込みを出力した場合、スケジューラは読み取りを並列化し、書き込みを直列化します。しかし、その順序を決定したのはモデルです。スケジューラはモデルの計画を強制しているのであって、独自の計画を立てているわけではありません。

デフォルトで安全側に倒す

最も一貫した設計原則: すべてが安全側に倒して失敗します。未知のツール? エラー。無効な入力? エラー。並行処理の宣言がない? 直列実行。権限の宣言がない? ユーザーに確認。機能フラグがない? ツールは存在しない。これは、主要なユーザーがツール名を幻覚したり入力を誤形成したりする可能性のある AI モデルであるシステムとしては異例です。システムはモデルの間違いを許容するのではなく、封じ込めるように設計されています。

拡張ポイントとしてのフック

フックシステム(ツール前、ツール後、および障害後)は主要な拡張ポイントです。これにより、組織はポリシーを適用し(ツール前フックの拒否ルール)、ログシステムはツールの使用状況をキャプチャし(ツール後フック)、CI/CD パイプラインは統合できます(障害フック)。重要なのは、フックは制限を強化することしかできず、緩和することはできないことです。フックは設定で許可されているツールを拒否することはできますが、設定で拒否されているツールを許可することはできません。

43 ツール、1 つのインターフェース

おそらく最も印象的なのは統一性です。bash コマンド、web_fetch、サブエージェントの起動、cron ジョブの作成、プッシュ通知はすべて、同じ 30 メソッドのインターフェースを実装し、同じ 7 フェーズのパイプラインを通過し、同じ権限システムに従います。ディスパッチャには特別なケースはありません。複雑さは個々のツールの実装と権限ルールにあり、ルーティングにはありません。

ワンクリック保存

YouMindでバイラル記事をAI深読み

ソースを保存し、的を絞った質問をし、主張を要約して、バイラル記事を再利用できるノートに変えます。すべてを1つのAIワークスペースで行えます。

YouMindを探索
クリエイターのために

あなたの Markdown をきれいな 𝕏 記事に

自分の長文を投稿するとき、画像・表・コードブロックを 𝕏 向けに整形するのは手間がかかります。YouMind は Markdown 全体を、そのまま投稿できるきれいな 𝕏 記事に変換します。

Markdown → 𝕏 を試す

解読すべきパターンをもっと

最近のバイラル記事

バイラル記事をもっと見る