# 拡張の仕組み：MCP・フック・スキル・プラグイン・Agent SDK

> Claude Code の拡張点を、エージェントループのどこに作用するかで分類する。MCP の JSON-RPC メッセージと版の取り決め、フックの終了コードと決定の合成、スキルと遅延読み込みの文脈コスト、設定の優先順位を、公式の仕様を模型にして証明つきで調べる。
> https://rikai.mugen-giken.com/computer-science/claude-code-internals/extension-mechanisms

## 0. この記事の要点

Claude Code に自分用の道具や決まりごとを足したいとき、どこに何を差し込めるのでしょうか。この記事は、足し方を 6 通りに分け、それぞれがエージェントの動きのどの場面に効くのかを調べます。読み終えると、「この決まりはモデルに頼めば足りるのか、確実に守らせたいのか」に応じて、使う仕組みを選べるようになります。

- 拡張の仕組みは、エージェントループのどこに作用するかで分けられます。モデルに見せるツールを増やす（MCP）、決まった時点で外のプログラムを走らせる（フック）、必要なときに指示を文脈に足す（スキル）、別のループに任せる（サブエージェント）、これらを束ねて配る（プラグイン）、ループ自体を自分のプログラムから動かす（Agent SDK）の 6 つです。
- MCP のメッセージは JSON-RPC 2.0（要求と応答を JSON で書く、呼び出しの取り決め）の要求・応答・通知です。未応答の要求の id が重ならなければ、応答と要求の対応は届く順に依らず一意に決まります（<Ref to="prop-jsonrpc-correlation" />）。
- MCP の仕様は日付の版を持ち、現行の 2026-07-28 版で接続の手順が変わりました。握手による取り決めは、双方に共通の版があっても成立しないことがあり、要求ごとに版を示す方式は共通の版があれば高々 2 回の要求で成立します（<Ref to="prop-version-negotiation" />）。
- ツール実行前のフックがブロックするエラー（終了コード 2。プログラムが終わるときに返す番号で、0 は成功を表す）で終われば、その呼び出しは権限規則やモードに依らず実行されません。フックのプログラムが異常終了したときに止まるか進むかは設定で決まります（<Ref to="prop-hook-exit-semantics" />）。
- フックが実行されることはモデルの出力に依りません。指示ファイルに書いた指示に従うかどうかはモデルの出力に依ります（<Ref to="prop-hook-determinism" />）。
- この記事の命題は、公式の仕様とドキュメントの記述を模型として定式化したものの中での命題です。MCP についての記述は、節ごとにどの版に基づくかを明記します。出典の確認日は 2026-10-10 です。

<Aside type="tip" title="この記事で使う事前知識">

- **エージェントループの模型**: モデルを呼び、ツール呼び出しを実行し、結果を返す、という繰り返しを写像で表した模型。→ [エージェントループの模型](/computer-science/claude-code-internals/agent-loop#def-agent-loop-model)
- **ツール呼び出し**: モデルが出す「このツールをこの引数で実行したい」という要求。→ [ツール呼び出し](/computer-science/claude-code-internals/tools-and-permissions#def-tool-call)
- **対応の不変条件**: 各ツール呼び出しに、同じ id の結果がちょうど 1 つ対応しているという条件。→ [対応の不変条件](/computer-science/claude-code-internals/agent-loop#def-pairing-invariant)
- **コンテキストウィンドウ**: モデルが一度に見られるメッセージ列の範囲。→ [コンテキストウィンドウ](/computer-science/claude-code-internals/context-window-and-prompt-caching#def-context-window)
- **単射**: 異なる元を必ず異なる元に送る写像（3.1 節の命題で使います）。
- **全順序集合**: どの 2 つの元も大小を比べられる集合（3.2 節と 6 節で使います）。

</Aside>

## 1. 動機

[第 1 章](/computer-science/claude-code-internals/agent-loop) で、エージェントループを「モデルを呼ぶ → ツール呼び出しを実行する → 結果を返す」の繰り返しとして定義しました。[第 2 章](/computer-science/claude-code-internals/tools-and-permissions) は実行の直前の関門（権限）を、[第 3 章](/computer-science/claude-code-internals/context-window-and-prompt-caching) と [第 4 章](/computer-science/claude-code-internals/compaction-memory-subagents) はモデルに渡すメッセージ列の扱いを調べました。

この章は、利用者がこのループに何かを**差し込む**仕組みを扱います。どの仕組みも、ループの決まった場所に作用します。場所が分かれば、その仕組みで何が保証でき、何がモデルの出力しだいで決まるかが分かります。たとえば「`.env` を編集しない」という決まりは、指示ファイルに書くこともフックで止めることもできますが、2 つは作用する場所が違い、保証の強さが違います。

拡張は、便利さと引き換えに 2 つのものを増やします。1 つは外から文脈に入る内容（MCP サーバの応答、ツールの説明、スキルの本文）、もう 1 つは利用者の権限で自動的に動くプログラム（フック、MCP サーバのプロセス）です。[次章](/computer-science/claude-code-internals/safety-and-trust-boundaries) の安全性の議論は、この章の一覧を対象にします。

<Aside type="note">
この章の数値（上限・既定値・イベントの名前）は、2026-10 時点の公式ドキュメントによります。フックのイベントや MCP の能力の一覧は版で増減するので、主要なものだけを挙げ、全体は出典に任せます。
</Aside>

## 2. 拡張点の分類

<Definition id="def-extension-points" title="拡張点">
エージェントループに対する**拡張点**を、作用する場所で次のように分類する。

| 仕組み | 作用する場所 | 増やすもの |
|---|---|---|
| MCP サーバ | モデルに渡すツール定義の集合と、ツールの実行 | モデルが呼べるツール。その結果として文脈に入る内容 |
| フック | ループの決まった時点（ツール実行の前後、利用者の入力時、停止時、圧縮の前など） | ハーネスが実行するプログラム |
| スキル | モデルに渡すメッセージ列 | 必要なときに文脈に足される指示 |
| サブエージェント | ループそのもの（別のメッセージ列を持つループを起動する） | 別のコンテキストウィンドウ |
| プラグイン | 上の 4 つをまとめて導入する単位 | 配布の単位 |
| Agent SDK | ループの外側（自分のプログラムからループを動かす） | ループの呼び出し元 |

</Definition>

サブエージェントは前章で扱ったので、この章では残りを順に見ます。公式ドキュメント（"Extend Claude Code"、2026-10-10 確認）は、同じものが複数の階層で定義されたときの重なり方が仕組みごとに違うことを述べています。CLAUDE.md は加法的（すべての階層の内容が文脈に入る）、スキルとサブエージェントと MCP サーバは名前で上書き（1 つの定義が勝つ）、フックは併合（登録されたものがすべて発火する）です。この違いは 6 節で扱います。

<Figure caption="エージェントループと拡張点。破線は拡張が作用する場所。">
<svg viewBox="0 0 680 230" width="100%" role="img" aria-label="エージェントループの各段階と、MCP・フック・スキルが作用する場所の図">
  <rect x="20" y="80" width="130" height="50" fill="none" stroke="currentColor" stroke-width="1.5" />
  <text x="85" y="110" font-size="13" text-anchor="middle" fill="currentColor">モデルの呼び出し</text>
  <rect x="200" y="80" width="130" height="50" fill="none" stroke="currentColor" stroke-width="1.5" />
  <text x="265" y="103" font-size="13" text-anchor="middle" fill="currentColor">実行前のフック</text>
  <text x="265" y="121" font-size="13" text-anchor="middle" fill="currentColor">権限の判定</text>
  <rect x="380" y="80" width="130" height="50" fill="none" stroke="currentColor" stroke-width="1.5" />
  <text x="445" y="110" font-size="13" text-anchor="middle" fill="currentColor">ツールの実行</text>
  <rect x="560" y="80" width="100" height="50" fill="none" stroke="currentColor" stroke-width="1.5" />
  <text x="610" y="110" font-size="13" text-anchor="middle" fill="currentColor">実行後のフック</text>
  <line x1="150" y1="105" x2="200" y2="105" stroke="currentColor" stroke-width="1.5" />
  <line x1="330" y1="105" x2="380" y2="105" stroke="currentColor" stroke-width="1.5" />
  <line x1="510" y1="105" x2="560" y2="105" stroke="currentColor" stroke-width="1.5" />
  <polyline fill="none" stroke="currentColor" stroke-width="1.5" points="610,130 610,180 85,180 85,130" />
  <text x="350" y="198" font-size="12" text-anchor="middle" fill="currentColor">結果をメッセージ列に足して、次の呼び出しへ</text>
  <line x1="85" y1="40" x2="85" y2="80" stroke="var(--sl-color-accent)" stroke-width="1.5" stroke-dasharray="5 4" />
  <text x="85" y="30" font-size="12" text-anchor="middle" fill="currentColor">スキル（指示を足す）・ツール定義</text>
  <line x1="445" y1="40" x2="445" y2="80" stroke="var(--sl-color-accent)" stroke-width="1.5" stroke-dasharray="5 4" />
  <text x="445" y="30" font-size="12" text-anchor="middle" fill="currentColor">MCP サーバ（tools/call）</text>
</svg>
</Figure>

## 3. MCP：ツールを外から足すプロトコル

### 3.1. 役割とメッセージ

Model Context Protocol（MCP）は、AI アプリケーションと、ツールやデータを提供するプログラムとの間の通信の取り決め（<Ref to="computer-science/software-engineering/networking-tcp-ip#def-protocol" text="プロトコル" />）です。

<Definition id="def-mcp-roles" title="MCP の役割">
MCP の**ホスト**は、1 つ以上のクライアントを管理する AI アプリケーションである。**クライアント**は、1 つのサーバへの接続を保ち、そのサーバから文脈を得る部品で、ホストはサーバごとにクライアントを 1 つ作る。**サーバ**は、クライアントに文脈（ツール・リソース・プロンプト）を提供するプログラムである。
</Definition>

この定義は、MCP の公式の概説（"Architecture overview"、2026-07-28 版に基づく記述、2026-10-10 確認）によります。Claude Code はホストにあたります。プロトコルは 2 つの層からなります。内側の**データ層**は JSON-RPC 2.0 に基づくメッセージの形と意味を決め、外側の**輸送層**は通信路を決めます。輸送路は、同じ機械の上の[プロセス](/computer-science/cs-basics/operating-systems#def-process)どうしが標準入出力でやり取りする stdio と、HTTP（Web の通信規約）を使う Streamable HTTP の 2 つです。同じ JSON-RPC のメッセージを、どちらの輸送路でも運べます。これは、上の層のデータを下の層の入れ物に入れて運ぶ <Ref to="computer-science/software-engineering/networking-tcp-ip#def-encapsulation" text="カプセル化" /> の一例です。

<Definition id="def-jsonrpc-messages" title="JSON-RPC のメッセージ">
MCP のメッセージは、次の 3 種類の JSON オブジェクトである（JSON-RPC 2.0。MCP の仕様の 2025-11-25 版と 2026-07-28 版に共通）。

- **要求**: `jsonrpc`（値は `"2.0"`）、`id`（文字列か整数。素の JSON-RPC 2.0 と違い、MCP では null は不可）、`method`（文字列）、任意の `params` を持つ。
- **応答**: `jsonrpc`、対応する要求と同じ `id`、および `result` か `error` のどちらか一方を持つ。`error` は整数の `code` と文字列の `message` を持つ。
- **通知**: `jsonrpc`、`method`、任意の `params` を持ち、`id` を持たない。受け手は応答を返してはならない。
</Definition>

`id` についての要請は版で違います。2025-11-25 版は「要求の ID は、同じセッションの中で要求側が以前に使ったものであってはならない」、2026-07-28 版は「要求の ID は、送り手が出してまだ応答を受け取っていない他の要求の ID と一致してはならない」と定めています（どちらも MUST NOT）。後者のほうが弱い要請ですが、次の命題にはそれで足ります。

次の命題のねらいは、要求を何本も続けて送っても、返ってきた応答がどの要求のものかを id で取り違えない条件を、集合と写像で言い直すことです。

<Proposition id="prop-jsonrpc-correlation" title="id による応答と要求の対応の一意性">
ある時点で、1 つの送り手が出してまだ応答を受け取っていない要求の集合を $Q$ とし、$\mathrm{id} : Q \to \mathrm{Id}$ を各要求の id とする。$\mathrm{id}$ が単射（$Q$ の相異なる要求は相異なる id を持つ）ならば、次が成り立つ。

1. 各応答 $\rho$ について、$\mathrm{id}(q) = \mathrm{id}(\rho)$ となる $q \in Q$ は高々 1 つである。
2. $Q$ の要求への応答がどの順序で届いても、「応答から要求への対応」は同じ写像になる。
3. 通知は id を持たないので、この対応の定義域にも値域にも入らない。

$\mathrm{id}$ が単射でなければ、1 は成り立たないことがある。
</Proposition>

<Proof of="prop-jsonrpc-correlation">
1: $q, q' \in Q$ がともに $\mathrm{id}(q) = \mathrm{id}(q') = \mathrm{id}(\rho)$ を満たすとすると、単射性から $q = q'$ です。

2: 対応を $c(\rho) = $「$\mathrm{id}(q) = \mathrm{id}(\rho)$ となるただ 1 つの $q$」と定めます。この定義は $\rho$ の id と写像 $\mathrm{id}$ だけを使い、届いた順序を使っていません。したがって、どの順序で届いても $c$ は同じです。これは、識別子が一意なら、表への登録の順序に依らず解決の結果が決まるという定理（<Ref to="computer-science/software-design-testing/case-study-textbook-checks#thm-reference-resolution" />）と同じ型の議論です。

3: 通知は id を持たないので、$\mathrm{id}(\rho)$ も $\mathrm{id}(q)$ も定義されません。

単射でない場合の反例: $Q = \{q_1, q_2\}$ で $\mathrm{id}(q_1) = \mathrm{id}(q_2) = 1$ とします。id が $1$ の応答 $\rho$ に対して、条件を満たす $q$ は 2 つあり、どちらの要求への応答かが決まりません。
</Proof>

2026-07-28 版の要請（未応答の要求どうしで id が重ならない）は、ちょうどこの命題の仮定です。2025-11-25 版の要請（セッションの中で再利用しない）はそれより強く、仮定を含みます。応答を待たずに次の要求を送れる（複数のツール呼び出しを並行に出せる）のは、この対応が順序に依らないからです。

### 3.2. 版と、版の取り決め

MCP の仕様の版は `YYYY-MM-DD` の形の文字列で、後方互換でない変更が最後に入った日を表します。公式の "Versioning" のページによれば、**現行（Current）の版は 2026-07-28** です（2026-10-10 確認）。この版で、接続の手順が大きく変わりました。次の表は、2025-11-25 版と 2026-07-28 版の本文、および 2026-07-28 版の変更点の一覧を読んでまとめたものです。

| 事柄 | 2025-11-25 版（握手による方式の最後の版） | 2026-07-28 版（現行） |
|---|---|---|
| 接続の初め | クライアントが `initialize` 要求を送り、サーバが応答し、クライアントが `notifications/initialized` を送る | 握手は無い。プロトコルは状態を持たない |
| 版と能力の伝え方 | `initialize` の要求と応答で 1 回 | すべての要求の `params._meta` に、版とクライアントの能力を入れる（必須） |
| 版が合わないとき | サーバが自分の対応する別の版を返す。クライアントが対応していなければ切断する | サーバが、対応する版の一覧つきのエラー（コード $-32022$）を返す。クライアントは共通の版で再試行する |
| サーバの情報を得る方法 | `initialize` の応答 | `server/discover`（サーバは実装しなければならない。呼ぶかどうかは任意） |
| セッション | プロトコルの水準のセッションがある | 無い。呼び出しをまたぐ状態は、サーバが発行した識別子をツールの引数として渡す |
| 要求の向き | クライアントからサーバへ、およびその逆 | クライアントからサーバへ。サーバが追加の入力を要るときは、`resultType` が `"input_required"` の結果を返し、クライアントが元の要求を再試行する |
| 結果 | `resultType` は無い | すべての結果が `resultType` を持つ（通常は `"complete"`）。一覧の結果は `ttlMs` と `cacheScope` を持つ |
| 能力 | クライアント側に roots・sampling・elicitation など、サーバ側に prompts・resources・tools・logging・completions など | Roots・Sampling・Logging は非推奨。`ping`、`logging/setLevel` などは削除 |

**Claude Code がどの版を使うか**について、公式ドキュメント（"Connect Claude Code to tools via MCP"、2026-10-10 確認）は次のことを述べています。Claude Code は 2 つのクライアントのランタイムのどちらかで MCP サーバに接続し、新しいほうのランタイムは MCP のプロトコルの 2026-07-28 版に対応する。新しいランタイムでは、HTTP・stdio・claude.ai のコネクタのサーバに新しい版に対応しているかを尋ね、対応しているサーバとはその版を使い、それ以外のサーバには従来のランタイムと同じように接続する。どちらのランタイムを使うかは、Claude Code の版、セッションの種類、環境変数で決まる（特定のサーバを従来の版にとどめる切り替えを Anthropic の側が持つことも書かれています）。従来の接続で使う版の日付は、このページでは確認できませんでした。

次の命題のねらいは、双方に共通の版があっても握手による方式では合意に失敗することがあり、要求ごとに版を示す方式なら高々 2 回で合意できる、という表の違いを確かめることです。$C$ はクライアントが対応する版の集合、$S$ はサーバが対応する版の集合で、1〜3 が握手による方式、4 が要求ごとの方式です。

<Proposition id="prop-version-negotiation" title="版の取り決めが成立する条件">
版の全体は全順序集合とし、クライアントが対応する版の有限集合を $C$、サーバが対応する版の有限集合を $S$ とする（どちらも空でない）。

1. （握手による方式。2025-11-25 版の Lifecycle の規則を模型にしたもの）クライアントが版 $c^{\ast} \in C$ を送り、サーバは $c^{\ast} \in S$ なら $c^{\ast} $ を、そうでなければある $s^{\ast} \in S$ を返し、クライアントは返された版が $C$ に入っていなければ切断する。このとき取り決めが成立するのは、$c^{\ast} \in S$ または $s^{\ast} \in C$ のとき、かつそのときに限る。
2. 1 の方式で $C \cap S = \varnothing$ なら成立しない。$C \cap S \ne \varnothing$ でも、双方が自分の最新の版を出す（$c^{\ast} = \max C$、$s^{\ast} = \max S$）と成立しないことがある。
3. 1 の方式で双方が最新の版を出すとき、$C$ と $S$ がどちらも版の全順序の中の区間（間の版をすべて含む）で $C \cap S \ne \varnothing$ ならば、成立する。
4. （要求ごとに版を示す方式。2026-07-28 版の規則を模型にしたもの）クライアントが版 $c \in C$ を付けて要求を送り、サーバは $c \in S$ なら処理し、そうでなければ $S$ を載せたエラーを返し、クライアントは $C \cap S$ から版を選んで再試行する。$C \cap S \ne \varnothing$ ならば、高々 2 回の要求で成立する。$C \cap S = \varnothing$ なら成立しない。
</Proposition>

<Proof of="prop-version-negotiation">
1: $c^{\ast} \in S$ なら、サーバは $c^{\ast}$ を返し、$c^{\ast} \in C$ なのでクライアントは切断せず、成立します。$c^{\ast} \notin S$ なら、サーバは $s^{\ast}$ を返し、クライアントが切断しないのは $s^{\ast} \in C$ のときだけです。よって成立は「$c^{\ast} \in S$ または $s^{\ast} \in C$」と同値です。

2: 成立したとき、合意した版は $c^{\ast}$（$\in C \cap S$）か $s^{\ast}$（$\in S \cap C$）で、どちらも $C \cap S$ の元です。したがって $C \cap S = \varnothing$ なら成立しません。後半の例: 版 $a \lt b \lt c$ について $C = \{a, c\}$、$S = \{a, b\}$ とします。$C \cap S = \{a\}$ は空でありません。$c^{\ast} = \max C = c \notin S$ なので、サーバは $s^{\ast} = \max S = b$ を返します。$b \notin C$ なので、クライアントは切断します。

3: $C = [c_1, c_2]$、$S = [s_1, s_2]$ と書きます（区間の両端）。$C \cap S \ne \varnothing$ から、共通の元 $x$ があり、$c_1 \le x \le c_2$、$s_1 \le x \le s_2$ です。$c_2 \le s_2$ の場合、$s_1 \le x \le c_2 \le s_2$ なので $c_2 \in S$（$S$ は区間）、すなわち $c^{\ast} = \max C \in S$ で、1 により成立します。$s_2 \lt c_2$ の場合、$c_1 \le x \le s_2 \lt c_2$ なので $s_2 \in C$、すなわち $s^{\ast} = \max S \in C$ で、1 により成立します。

4: 1 回目の要求の版 $c$ が $S$ に入っていれば、その要求が処理されて成立します（1 回）。入っていなければ、サーバは $S$ を載せたエラーを返します。クライアントは $C$ を知っているので $C \cap S$ を計算でき、空でなければその元 $c'$ を選んで再試行します。$c' \in S$ なので 2 回目の要求は処理されます（合計 2 回）。$C \cap S = \varnothing$ なら、どの $c \in C$ も $S$ に入らないので、どの要求も処理されません。
</Proof>

握手による方式が失敗しうるのは、サーバが「自分の対応する別の版」を 1 つしか返さないからです。現行の方式ではサーバが集合 $S$ をまるごと返すので、クライアントが共通部分を計算できます。これは、依存の版の制約を満たす版を探す問題（<Ref to="computer-science/software-design-testing/build-systems-and-dependencies#def-version-resolution" />）で、候補の集合が見えていれば解を選べる、というのと同じ事情です。MCP の版は日付で、数字の組に意味を持たせる方式（<Ref to="computer-science/software-design-testing/build-systems-and-dependencies#def-semver" />）とは違い、版の文字列から互換性は読み取れません。

<Remark id="rem-negotiation-scope" title="模型に入れていないこと">
4 は、クライアントとサーバがどちらも現行の方式で話す場合の命題です。握手を待つサーバと現行の方式のクライアントの組み合わせなど、方式そのものが食い違う場合について、2026-07-28 版は互換性の表を載せています（両方の方式に対応するクライアントは、stdio ではまず `server/discover` を送り、現行の方式のエラーとして認識できない応答や無応答なら握手に切り替える、など）。この表の内容は命題にはしていません。また 1 の「切断する」は仕様では SHOULD、「最新の版を出す」も SHOULD で、必ずそうするとは限りません。
</Remark>

### 3.3. ツールの一覧と呼び出し、2 種類のエラー

<Definition id="def-mcp-tools" title="MCP のツール">
サーバは、`tools/list` 要求に対して、利用できるツールの定義の配列を返す。ツールの定義は `name`、`description`、`inputSchema`（引数の JSON Schema）などからなる。クライアントは `tools/call` 要求に `name` と `arguments` を入れてツールを呼び出し、サーバは `content`（text などのブロックの配列）と任意の `isError` を持つ結果を返す。エラーの伝え方は 2 つある。

- **プロトコルのエラー**: 要求の形そのものの問題（未知のツール名、形の不正な要求、サーバのエラー）。JSON-RPC の `error` を持つ応答として返る。
- **ツール実行のエラー**: ツールを実行した結果の失敗（API の失敗、入力値の検証の失敗、業務上の誤り）。`isError` が `true` の**結果**として返る。
</Definition>

この定義は 2026-07-28 版の "Tools" のページによります（`tools/list`・`tools/call` と 2 種類のエラーは、2025-06-18 版・2025-11-25 版にもあります）。同じページは、クライアントはツール実行のエラーを言語モデルに渡すべきである（SHOULD）、プロトコルのエラーは渡してもよい（MAY）と定めています。安全性に関わる要請として、次の 2 つがあります（次章で使います）。

- ツールは「モデルが制御する」ものとして設計されているが、ツールの呼び出しを拒否できる人間が常にループの中にいるべきである（SHOULD）。
- クライアントは、信頼できるサーバからのものでない限り、ツールの注釈（annotations）を信頼できないものとして扱わなければならない（MUST）。

2026-07-28 版には、`tools/list` は決定的な順序でツールを返すべきである（SHOULD）という要請も加わりました。理由として、クライアント側のキャッシュと、LLM のプロンプトキャッシュの命中率が挙げられています。ツール定義は要求の先頭の層にあるので、順序が変わるとその後ろのキャッシュがすべて命中しなくなる（<Ref to="computer-science/claude-code-internals/context-window-and-prompt-caching#prop-prefix-invalidation" />）ことに対応した要請です。

ツールの実行が失敗しても、モデルに返す結果が欠けるとループは先へ進めません。次の命題のねらいは、失敗を「結果」として返せば、第 1 章の[対応の不変条件](/computer-science/claude-code-internals/agent-loop#def-pairing-invariant)（各 tool_use に同じ id の tool_result がちょうど 1 つある）が保たれ、結果が欠ける場合には破れる、と確かめることです。

<Proposition id="prop-two-error-channels" title="エラーを結果として返すと、ループの不変条件が保たれる">
[第 1 章](/computer-science/claude-code-internals/agent-loop) の模型で、あるツールの実行を MCP の `tools/call` で行うとする。`tools/call` の帰結は、(i) `isError` が偽の結果、(ii) `isError` が真の結果、(iii) プロトコルのエラー、(iv) 応答なし（時間切れ）のどれかである。クライアントが帰結を tool_result に写す規則を $\theta$ とする。

1. $\theta$ が (i)・(ii) について「結果の `content` を内容とし、`isError` をエラーの印とする tool_result を 1 つ作る」と定められていれば、(i)・(ii) の帰結のもとで、tool_use と tool_result の対応の不変条件は保たれる。エラーの場合もループは続けられる。
2. $\theta$ が (iii)・(iv) について定められていなければ、その帰結のもとで対応する tool_result が作られず、不変条件は破れる。$\theta$ がすべての帰結について tool_result をちょうど 1 つ作るように定められていれば、不変条件は保たれる。
</Proposition>

<Proof of="prop-two-error-channels">
第 1 章の模型では、ツールの実行は「入力と環境から、結果の文字列・エラーの印・新しい環境への写像」で、すべての入力に値を持つ（全域である）ことが、不変条件の保存（<Ref to="computer-science/claude-code-internals/agent-loop#prop-tool-result-pairing" />）の証明で使われています。各 tool_use に対して実行が値を返すので、同じ id の tool_result がちょうど 1 つ作られる、という議論です。

1: (i)・(ii) の帰結に対して $\theta$ は tool_result を 1 つ返すので、この 2 つの帰結に限れば実行は値を持ち、第 1 章の命題の議論がそのまま使えます。(ii) はエラーの印が立った tool_result になるだけで、拒否を結果として返す場合（<Ref to="computer-science/claude-code-internals/agent-loop#prop-denied-call-continues" />）と同じ形です。

2: (iii)・(iv) の帰結で $\theta$ の値が無ければ、その tool_use に対応する tool_result が次の user メッセージに現れず、「各 tool_use に同じ id の tool_result がちょうど 1 つある」という条件が成り立ちません。$\theta$ がすべての帰結で tool_result をちょうど 1 つ作るなら、実行は全域の写像になり、第 1 章の命題が適用できます。
</Proof>

この命題は、仕様が失敗を 2 つに分けている理由の 1 つを説明します。モデルが引数を直せば解決する失敗は、結果としてモデルに見せる（(ii)）。要求の形そのものの問題は、プロトコルの水準で返す（(iii)）。ただし (iii) と (iv) でも、ループを続けるには、クライアントが何らかの tool_result を自分で作る必要があります。

### 3.4. 最小のサーバとクライアント

2 つの数の和を返すツール `add` を 1 つだけ持つ、最小の MCP サーバを書きます。**仕様の 2026-07-28 版の形**（握手なし、要求ごとの `_meta`、`server/discover`、`resultType`、一覧の `ttlMs` と `cacheScope`）に合わせています。輸送路は stdio で、1 行が 1 つのメッセージです。Python の標準ライブラリだけを使い、公式の SDK は使っていません。仕様の一部（ページ分割、購読、追加の入力の要求など）は実装していないので、仕様に適合したサーバではなく、メッセージの形を確かめるための模型です。

```python
# mcp_server.py — 最小の MCP サーバ（標準入出力）。仕様の版 2026-07-28 の形に合わせた模型。
# 握手（initialize）は無い。版と能力は要求ごとに params._meta で届く。
import json, sys

VERSIONS = ["2026-07-28"]
P = "io.modelcontextprotocol/"
TOOLS = [{
    "name": "add", "description": "2 つの数の和を返す",
    "inputSchema": {"type": "object",
                    "properties": {"a": {"type": "number"}, "b": {"type": "number"}},
                    "required": ["a", "b"]},
}]

def error(rid, code, message, data=None):
    e = {"code": code, "message": message}
    if data is not None:
        e["data"] = data
    return {"jsonrpc": "2.0", "id": rid, "error": e}

def handle(req):
    rid, method, params = req.get("id"), req.get("method"), req.get("params", {})
    meta = params.get("_meta", {})
    if P + "protocolVersion" not in meta or P + "clientCapabilities" not in meta:
        return error(rid, -32602, "Invalid params: missing required _meta fields")
    if meta[P + "protocolVersion"] not in VERSIONS:
        return error(rid, -32022, "Unsupported protocol version",
                     {"supported": VERSIONS, "requested": meta[P + "protocolVersion"]})
    if method == "server/discover":
        result = {"supportedVersions": VERSIONS, "capabilities": {"tools": {}},
                  "ttlMs": 3600000, "cacheScope": "public"}
    elif method == "tools/list":
        result = {"tools": TOOLS, "ttlMs": 300000, "cacheScope": "public"}
    elif method == "tools/call":
        if params.get("name") != "add":                      # プロトコルのエラー
            return error(rid, -32602, "Unknown tool: " + str(params.get("name")))
        args = params.get("arguments", {})
        a, b = args.get("a"), args.get("b")
        ok = all(isinstance(v, (int, float)) and not isinstance(v, bool) for v in (a, b))
        if ok:
            result = {"content": [{"type": "text", "text": str(a + b)}], "isError": False}
        else:                                                # ツール実行のエラー
            result = {"content": [{"type": "text", "text": "a と b は数でなければならない"}],
                      "isError": True}
    else:
        return error(rid, -32601, "Method not found")
    result["resultType"] = "complete"
    result["_meta"] = {P + "serverInfo": {"name": "add-server", "version": "0.1.0"}}
    return {"jsonrpc": "2.0", "id": rid, "result": result}

for line in sys.stdin:                   # 1 行 = 1 メッセージ
    msg = json.loads(line)
    if "id" not in msg:                  # 通知には応答しない
        continue
    sys.stdout.write(json.dumps(handle(msg), ensure_ascii=False) + "\n")
    sys.stdout.flush()
```

クライアントは、このサーバを子プロセスとして起動し、要求を送って、応答を id で突き合わせます。

```python
# mcp_client.py — 最小のクライアント。要求を送り、応答を id で突き合わせる。
import json, subprocess, sys

P = "io.modelcontextprotocol/"

def meta(version):
    return {P + "protocolVersion": version, P + "clientCapabilities": {},
            P + "clientInfo": {"name": "mini-client", "version": "0.1.0"}}

proc = subprocess.Popen([sys.executable, "mcp_server.py"], stdin=subprocess.PIPE,
                        stdout=subprocess.PIPE, universal_newlines=True)
pending, next_id = {}, 1

def send(method, params=None, version="2026-07-28"):
    global next_id
    params = dict(params or {})
    params["_meta"] = meta(version)
    rid, next_id = next_id, next_id + 1          # 未応答の要求と重ならない id
    pending[rid] = method
    proc.stdin.write(json.dumps({"jsonrpc": "2.0", "id": rid, "method": method,
                                 "params": params}) + "\n")
    proc.stdin.flush()
    return rid

def receive():
    resp = json.loads(proc.stdout.readline())
    method = pending.pop(resp["id"])             # id から要求を一意に引く
    return resp["id"], method, resp

# 1. 版が合わない要求 → 対応する版の一覧つきのエラー → その版で再試行（高々 2 回）
send("tools/list", version="2099-01-01")
rid, method, resp = receive()
print(rid, method, "->", resp["error"]["code"], resp["error"]["data"])
version = resp["error"]["data"]["supported"][0]

# 2. 4 つの要求をまとめて送り、あとから応答を id で突き合わせる
send("tools/list", version=version)
send("tools/call", {"name": "add", "arguments": {"a": 2, "b": 3}}, version)
send("tools/call", {"name": "add", "arguments": {"a": 2, "b": "x"}}, version)
send("tools/call", {"name": "mul", "arguments": {"a": 2, "b": 3}}, version)
proc.stdin.write(json.dumps({"jsonrpc": "2.0", "method": "notifications/cancelled",
                             "params": {"requestId": 999}}) + "\n")   # 通知: 応答は来ない
proc.stdin.flush()
for _ in range(4):
    rid, method, resp = receive()
    if "error" in resp:
        print(rid, method, "-> プロトコルのエラー", resp["error"]["code"], resp["error"]["message"])
    elif "tools" in resp["result"]:
        print(rid, method, "->", [t["name"] for t in resp["result"]["tools"]], resp["result"]["resultType"])
    else:
        r = resp["result"]
        print(rid, method, "-> isError =", r["isError"], r["content"][0]["text"])
proc.stdin.close()
proc.wait()
print("未応答の要求:", pending, " サーバの終了コード:", proc.returncode)
```

実行した出力（Python 3.9、2026-10-10 実行）:

```text
1 tools/list -> -32022 {'supported': ['2026-07-28'], 'requested': '2099-01-01'}
2 tools/list -> ['add'] complete
3 tools/call -> isError = False 5
4 tools/call -> isError = True a と b は数でなければならない
5 tools/call -> プロトコルのエラー -32602 Unknown tool: mul
未応答の要求: {}  サーバの終了コード: 0
```

1 行目と 2 行目は <Ref to="prop-version-negotiation" /> の 4（版が合わなくても 2 回目で成立）、4 行目と 5 行目は 2 種類のエラーの違い（引数の型の誤りは結果の中の印、未知のツール名はプロトコルのエラー）を示しています。通知には応答が無く、未応答の要求の表は空で終わっています。握手による方式のサーバを書く場合は、最初に `initialize` 要求に答え、`notifications/initialized` を受け取ってから他の要求を処理する、という状態が加わります。

`tools/call` の要求と、成功の応答を書き下すと次の形です（上のクライアントが実際に送受信したもの）。

```json
{"jsonrpc": "2.0", "id": 3, "method": "tools/call",
 "params": {"name": "add", "arguments": {"a": 2, "b": 3},
            "_meta": {"io.modelcontextprotocol/protocolVersion": "2026-07-28",
                      "io.modelcontextprotocol/clientCapabilities": {},
                      "io.modelcontextprotocol/clientInfo": {"name": "mini-client", "version": "0.1.0"}}}}
```

```json
{"jsonrpc": "2.0", "id": 3,
 "result": {"content": [{"type": "text", "text": "5"}], "isError": false,
            "resultType": "complete",
            "_meta": {"io.modelcontextprotocol/serverInfo": {"name": "add-server", "version": "0.1.0"}}}}
```

### 3.5. Claude Code での MCP と、ツール定義の遅延読み込み

公式ドキュメント（"Connect Claude Code to tools via MCP"、2026-10-10 確認）から、仕組みに関わる事実を挙げます。

- **輸送路**: HTTP（遠隔のサーバへの接続に推奨）、SSE（非推奨）、stdio（利用者の機械の上のプロセス）、WebSocket。
- **設定の範囲**: local（既定。そのプロジェクトで自分だけ）、project（プロジェクトのルートの `.mcp.json`。バージョン管理で共有）、user（自分のすべてのプロジェクト）。プロジェクトの範囲のサーバは、対話的なセッションでは使う前に承認を求められます。
- **ツールの名前**: MCP のツールは `mcp__<サーバ名>__<ツール名>` の形の名前でモデルに見え、権限規則やフックのマッチャでもこの名前を使います。
- **出力の上限**: MCP ツールの出力が 10,000 トークンを超えると警告が出て、既定の上限は 25,000 トークンです（環境変数 `MAX_MCP_OUTPUT_TOKENS` で変更）。
- **遅延読み込み**: 既定では、セッションの開始時にはツールの名前と、サーバが与える使い方の指示（server instructions）だけが読み込まれ、ツール定義は、モデルが必要としたときにツール検索を通じて読み込まれます。環境変数 `ENABLE_TOOL_SEARCH` で、すべて前もって読み込む・定義の合計がウィンドウの一定の割合（既定 10%）未満なら前もって読み込む、などに変えられます。ツールの説明とサーバの指示は、既定でそれぞれ 2,048 文字で切られます。遅延読み込みは、利用する API の接続先やモデルによっては働かず、そのときは前もって読み込まれます。
- **接続前の注意**: 接続する前に、そのサーバを信頼できるか確かめること、外部の内容を取得するサーバは[プロンプトインジェクション](/computer-science/claude-code-internals/safety-and-trust-boundaries#def-prompt-injection)（取り込んだ文章の中に紛れた指示にモデルが従ってしまう攻撃）の危険を持ち込みうること、が注意されています。

次の命題のねらいは、ツールが多いときに、定義を前もって全部置く方式と、名前だけ置いて使うときに定義を読む方式とで、文脈に置かれる量がどれだけ違うかを式にすることです。4 に出てくるブレークポイントは、プロンプトキャッシュ（前に送った先頭部分の処理を使い回す仕組み）で、先頭からどこまでを使い回すかを示す位置の印のことです。

<Proposition id="prop-deferred-tool-cost" title="ツール定義の常駐コスト">
$N$ 個のツールがあり、ツール $i$ の定義の長さを $d_i$、名前の長さを $m_i$（$m_i \le d_i$）とする。会話の中で使うツールの集合を $U$ とする。

1. 定義を前もって全部読み込む方式で、文脈に置かれるツールの記述の量は $\sum_{i=1}^{N} d_i$。
2. 名前だけを置き、使うときに定義を読み込む方式では $\sum_{i=1}^{N} m_i + \sum_{i \in U} d_i$。
3. 差（1 から 2 を引いたもの）は $\sum_{i \notin U} (d_i - m_i) - \sum_{i \in U} m_i$。定義と名前の長さがどれも $d$、$m$ で $|U| = u$ のとき、差は $(N - u)(d - m) - u\,m$ で、2 のほうが小さいのは $u \lt N\,(d - m)/d$ のときである。
4. 2 の方式で、定義の読み込みをメッセージ列の末尾への追記として行えば、それまでの要求は新しい要求の接頭辞のままで、既存のキャッシュのブレークポイントは命中し続ける。要求の先頭の層にあるツール定義の集合を途中で変える方式では、変えた位置より後ろのブレークポイントはすべて命中しない。
</Proposition>

<Proof of="prop-deferred-tool-cost">
1 と 2 は、文脈に置かれる文字列を数えただけです。2 では、すべてのツールの名前と、使ったツールの定義が置かれます。

3: $\sum_{i} d_i - \sum_i m_i - \sum_{i \in U} d_i = \sum_{i \notin U} d_i - \sum_{i} m_i = \sum_{i \notin U} (d_i - m_i) - \sum_{i \in U} m_i$ です。長さが一定のとき $(N - u)(d - m) - u\,m = N d - N m - u d$ で、これが正であることは $u\,d \lt N\,(d - m)$、すなわち $u \lt N\,(d - m)/d$ と同値です。

4: 末尾に追記するだけなら、前の要求は新しい要求の接頭辞です。<Ref to="computer-science/claude-code-internals/context-window-and-prompt-caching#prop-prefix-invalidation" /> により、内容が変わった位置より前のブレークポイントは影響を受けません。先頭の層のツール定義の集合を変えると、内容が変わる位置がその層の中にあり、同じ命題により、それより後ろのブレークポイントはすべて命中しません。
</Proof>

公式ドキュメント（"How Claude Code uses prompt caching"）は 4 の両方の場合を述べています。ツールが遅延されているときは、会話の最初の要求のツールの一覧を会話のあいだ保つので、途中でサーバが接続・切断しても既存のキャッシュは乱されない。前もって読み込んでいるときは、定義が足されるとキャッシュが無効になる。

<Example id="ex-two-hundred-tools" title="200 個のツールのうち 3 個しか使わないとき（仮の値）">
$N = 200$、定義の平均 $d = 300$ トークン、名前の平均 $m = 10$ トークン、$u = 3$ と置きます（**仮の値**）。前もって読み込むと $200 \times 300 = 60{,}000$ トークン、遅延すると $200 \times 10 + 3 \times 300 = 2{,}900$ トークンで、差は $57{,}100 = (200 - 3)(300 - 10) - 3 \times 10$ です。遅延のほうが小さいのは $u \lt 200 \times 290/300 \approx 193.3$ のとき、つまり 193 個以下しか使わないときです。次のコードの出力（Python 3.9、2026-10-10 実行）で確かめられます。

```python
N, u, d, m = 200, 3, 300, 10      # 個数、使う個数、定義の平均、名前の平均（仮の値）
upfront = N * d
deferred = N * m + u * d
print("前もって読み込む:", upfront, " 遅延:", deferred, " 差:", upfront - deferred,
      " = (N - u)(d - m) - u m =", (N - u) * (d - m) - u * m)
for uu in (0, 3, 50, 193, 194, 200):
    print(f"u = {uu:>3}: 遅延 = {N * m + uu * d:>6}  前もって = {upfront}")
```

```text
前もって読み込む: 60000  遅延: 2900  差: 57100  = (N - u)(d - m) - u m = 57100
u =   0: 遅延 =   2000  前もって = 60000
u =   3: 遅延 =   2900  前もって = 60000
u =  50: 遅延 =  17000  前もって = 60000
u = 193: 遅延 =  59900  前もって = 60000
u = 194: 遅延 =  60200  前もって = 60000
u = 200: 遅延 =  62000  前もって = 60000
```

この勘定には、定義を探して読み込むための呼び出しそのものの費用（1 回分の往復）は入れていません。
</Example>

## 4. フック：決まった時点で必ず走るプログラム

### 4.1. イベントとハンドラ

<Definition id="def-hook" title="フック">
**フック**とは、Claude Code のライフサイクルの決まった時点（**イベント**）で、Claude Code が自動的に実行する、利用者が定義したハンドラをいう。ハンドラは、シェルのコマンド、HTTP の送信先、MCP ツールの呼び出し、LLM へのプロンプト、サブエージェントのどれかである。設定は「イベント → マッチャの組 → ハンドラ」の 3 段の入れ子で書く。
</Definition>

公式の "Hooks reference"（2026-10-10 確認）から、主要なイベントを挙げます。全体は 30 種類を超え、版で増減するので、一覧は出典を見てください。

| イベント | 発火する時点 | 終了コード 2 の効果 |
|---|---|---|
| `SessionStart` | セッションの開始・再開のとき | ブロックできない |
| `UserPromptSubmit` | 利用者の入力が送られ、モデルが処理する前 | 入力を止める（モデルに届かない） |
| `PreToolUse` | ツール呼び出しを実行する前 | 呼び出しを止める |
| `PostToolUse` | ツール呼び出しが成功したあと | ブロックできない（ツールはすでに実行されている） |
| `Stop` | モデルが応答を終えたとき | 止まるのを防ぎ、会話を続けさせる |
| `PreCompact` | 圧縮の前 | 圧縮を止める |
| `SubagentStart` / `SubagentStop` | サブエージェントの起動時 / 終了時 | 起動は止められない / 終了を防ぐ |

コマンドのハンドラは、JSON を標準入力で受け取ります。共通の欄として `session_id`、`transcript_path`、`cwd`、`hook_event_name` などがあり、`PreToolUse` では `tool_name` と `tool_input` が加わります。**マッチャ**は、ハンドラをどの対象（ツールのイベントではツール名）で発火させるかを絞る文字列で、`"*"`・空文字列・省略はすべてにマッチし、英数字と `_`・`-`・空白・`,`・`|` だけからなるものは完全一致（または `|` や `,` で区切った列挙）、それ以外の文字を含むものは JavaScript の正規表現として評価されます。

設定の場所は、利用者の設定ファイル、プロジェクトの設定ファイル（共有するものとしないもの）、組織の管理設定、プラグインの `hooks/hooks.json`、スキルとサブエージェントの frontmatter です。マッチしたフックはすべて並行に実行され、同じハンドラが複数の設定ファイルにあれば 1 回だけ実行されます。既定のタイムアウトは、コマンド・HTTP・MCP ツールのハンドラが 600 秒、プロンプトが 30 秒、エージェントが 60 秒で、一部のイベント（`UserPromptSubmit` など）ではもっと短く設定されています。

### 4.2. 終了コードと決定

<Definition id="def-hook-outcome" title="コマンドのフックの帰結と決定">
コマンドのフックの 1 回の実行の**帰結**は、次の 3 つのどれかである（公式ドキュメントの記述による）。

- **成功**: 終了コード 0。標準出力に JSON を出していれば、その欄が適用される。
- **ブロックするエラー**: 終了コード 2。ブロックできるイベントでは、その行為が止まる。標準エラーが理由になる。
- **ブロックしないエラー**: それ以外の終了コード、または起動できない・不正な JSON を出すなどの失敗。行為は進む。ただし、そのフックに `onFailure` が `"block"` と設定されていれば、失敗のとき行為は止まる。

標準出力の内容で帰結が変わる場合がある。公式の表によれば、0 と 2 以外の終了コードでも、検査を通る JSON を標準出力に出していれば、帰結は成功で、JSON の欄だけで決まる（`onFailure` が `"block"` のときは失敗に数える）。終了コード 2 は、JSON を出していてもブロックする。以下の命題では、標準出力に JSON を出さないフックを考え、帰結が終了コードだけで決まる場合を扱う。

`PreToolUse` のフックは、成功のとき JSON の `hookSpecificOutput.permissionDecision` で**決定**（`"allow"`・`"deny"`・`"ask"`・`"defer"`）を返せる。複数の `PreToolUse` フックが異なる決定を返したときの優先順位は `deny` $\gt$ `defer` $\gt$ `ask` $\gt$ `allow` である。終了コード 2 でのブロックは `"deny"` と同じ経路をたどる。
</Definition>

決定の合成の規則（`deny` $\gt$ `defer` $\gt$ `ask` $\gt$ `allow`）は、"Hooks reference" の `PreToolUse` の節と、Agent SDK の "Intercept and control agent behavior with hooks" のページの両方に書かれています。`onFailure` は、公式ドキュメントによれば Claude Code v2.1.295 以降の機能で、コマンドと HTTP のフックに設定でき、既定値は `"continue"` です（2026-10 時点）。同じ節によれば、`Stop`・`SubagentStop` など一部のイベントのフックと、バックグラウンドで動かすコマンドのフックでは、この設定は効きません。

権限規則との関係は、[第 2 章](/computer-science/claude-code-internals/tools-and-permissions) で扱ったとおりです（<Ref to="computer-science/claude-code-internals/tools-and-permissions#prop-hook-composition" />）。公式ドキュメント（"Configure permissions"）は、`PreToolUse` フックの決定は権限規則を迂回しない（フックが `"allow"` を返しても、マッチする deny 規則は止め、ask 規則は確認を出す）こと、終了コード 2 で終わるフックは権限規則の評価の前に呼び出しを止めるので、allow 規則があっても止まることを述べています。

次の命題のねらいは、止めるためのフックが「どんな場合に呼び出しを確実に止めるか」を、終了コードと `onFailure` の設定から読み取ることです。fail-open は、失敗したときに止めずに先へ進める側に倒す設計、fail-closed は、失敗したときも止める側に倒す設計を指します。

<Figure caption="PreToolUse のコマンドのフックの帰結（JSON を出さない場合）。終了コードと onFailure の設定で、呼び出しが権限の判定へ進むか止まるかが分かれる。">
<svg viewBox="0 0 680 240" width="100%" role="img" aria-label="PreToolUse フックの終了コードごとに、権限の判定へ進むか、実行されないかを示す分岐図">
  <rect x="20" y="95" width="130" height="50" fill="none" stroke="currentColor" stroke-width="1.5" />
  <text x="85" y="117" font-size="13" text-anchor="middle" fill="currentColor">PreToolUse</text>
  <text x="85" y="135" font-size="13" text-anchor="middle" fill="currentColor">フックが終了</text>
  <line x1="150" y1="120" x2="200" y2="45" stroke="currentColor" stroke-width="1.5" />
  <line x1="150" y1="120" x2="200" y2="125" stroke="currentColor" stroke-width="1.5" />
  <line x1="150" y1="120" x2="200" y2="205" stroke="currentColor" stroke-width="1.5" />
  <rect x="200" y="20" width="230" height="50" fill="none" stroke="currentColor" stroke-width="1.5" />
  <text x="315" y="50" font-size="13" text-anchor="middle" fill="currentColor">終了コード 0（成功）</text>
  <rect x="200" y="100" width="230" height="50" fill="none" stroke="currentColor" stroke-width="1.5" />
  <text x="315" y="122" font-size="12" text-anchor="middle" fill="currentColor">その他の終了コード・起動失敗など</text>
  <text x="315" y="140" font-size="12" text-anchor="middle" fill="currentColor">onFailure が continue（既定）</text>
  <rect x="200" y="180" width="230" height="50" fill="none" stroke="currentColor" stroke-width="1.5" />
  <text x="315" y="202" font-size="12" text-anchor="middle" fill="currentColor">終了コード 2、または</text>
  <text x="315" y="220" font-size="12" text-anchor="middle" fill="currentColor">失敗で onFailure が block</text>
  <line x1="430" y1="45" x2="470" y2="45" stroke="currentColor" stroke-width="1.5" />
  <line x1="430" y1="125" x2="470" y2="125" stroke="currentColor" stroke-width="1.5" />
  <line x1="430" y1="205" x2="470" y2="205" stroke="currentColor" stroke-width="1.5" />
  <rect x="470" y="20" width="190" height="50" fill="none" stroke="currentColor" stroke-width="1.5" />
  <text x="565" y="50" font-size="13" text-anchor="middle" fill="currentColor">権限の判定へ</text>
  <rect x="470" y="100" width="190" height="50" fill="none" stroke="currentColor" stroke-width="1.5" />
  <text x="565" y="130" font-size="13" text-anchor="middle" fill="currentColor">権限の判定へ</text>
  <rect x="470" y="180" width="190" height="50" fill="none" stroke="var(--sl-color-accent)" stroke-width="2" />
  <text x="565" y="210" font-size="13" text-anchor="middle" fill="currentColor">呼び出しは実行されない</text>
</svg>
</Figure>

<Proposition id="prop-hook-exit-semantics" title="ブロックするフックの保証と、失敗時の向き">
<Ref to="def-hook-outcome" /> の規則を模型として置く。あるツール呼び出し $(n, x)$ について、マッチャが $n$ にマッチする `PreToolUse` のコマンドのフック $H$ が登録され、実行されるとする。ツールの判定に介入する mod（後述）は導入されていないとする。

1. $H$ の帰結がブロックするエラーなら、$(n, x)$ は実行されない。これは、権限規則の集合、権限モード、他のフックの決定に依らない。
2. 危険とみなす呼び出しの集合を $\mathrm{Bad}$ とし、$H$ は「$(n, x) \in \mathrm{Bad}$ ならブロックするエラーで終わる」ように書かれたプログラムとする。ところが、ある $(n, x) \in \mathrm{Bad}$ で $H$ が異常終了（ブロックしないエラー）したとする。`onFailure` が `"continue"`（既定）なら、$(n, x)$ は止められず、権限の判定に進む（fail-open）。`"block"` なら、$(n, x)$ は実行されない（fail-closed）。
3. したがって「$\mathrm{Bad}$ の呼び出しは実行されない」という保証が成り立つための $H$ への要求は、fail-open では「$\mathrm{Bad}$ のすべての入力で、ブロックするエラーで正しく終わる」こと、fail-closed では「$\mathrm{Bad}$ のすべての入力で、成功の帰結にならない」ことである。後者は前者より弱い。
</Proposition>

<Proof of="prop-hook-exit-semantics">
1: 規則により、終了コード 2 は `PreToolUse` では呼び出しを止め、`"deny"` と同じ経路をたどります。決定の合成で `deny` は最上位なので、他のフックがどの決定を返しても結果は `deny` です。また、このブロックは権限規則の評価の前に働くので、allow 規則やモードの既定は適用される機会がありません。

2: 異常終了はブロックしないエラーです。規則により、`onFailure` が `"continue"` なら行為は進み、$H$ は何の決定も与えなかったのと同じになります。呼び出しは、他のフックと権限規則の判定に進みます。`"block"` なら、規則により行為は止まります。

3: fail-open では、$\mathrm{Bad}$ の入力で $H$ の帰結が「ブロックするエラー」でなければ（成功でも、ブロックしないエラーでも）$H$ は呼び出しを止めないので、保証には「必ずブロックするエラーで終わる」ことが要ります。fail-closed では、ブロックするエラーとブロックしないエラーのどちらでも止まるので、「成功にならない」ことで足ります。「ブロックするエラーで終わる」ならば「成功にならない」ので、後者は前者より弱い条件です。
</Proof>

検査を目的とするフックでは、この違いが大きく効きます。公式ドキュメントは、既定の設定では、パスを間違えたフックや落ちるスクリプトがすべてを通してしまう、と注意しています。止めることが目的のフックは、失敗のときも止まる側に倒しておく（`onFailure` を `"block"` にする、または入力を読めない場合も終了コード 2 で終わるように書く）のが、この命題からの帰結です。一方、整形や通知のように副作用だけが目的のフックは、失敗しても作業を止めないほうが都合がよく、fail-open が合います。どちらにするかは、フックが担う役割（検査か、付随的な処理か）で決まります。これは、関数が満たすべき条件を呼ぶ側と呼ばれる側で取り決める <Ref to="computer-science/software-design-testing/contracts-and-invariants#def-contract" text="契約" /> の考え方と同じで、フックの「失敗のときの振る舞い」を契約の一部として決めておく、ということです。

<Remark id="rem-hook-exceptions" title="模型の外にあるもの">
1 の「依らない」には、公式ドキュメントが明記する例外があります。プラグインのうち、Claude Code の中で自分のコードを動かすもの（mod）は、ツールの判定に答えを返すことができ、設定ファイルやプラグインの `PreToolUse` フックのブロックを承認に置き換えられる（組織の管理設定のフックを除く）、と書かれています。命題に「mod は導入されていない」という仮定を入れたのはこのためです。また、フックは無効にでき（`disableAllHooks`）、対話的なセッションではフォルダの信頼を確認するまで設定ファイルのフックは実行されません。SDK のコールバックとして登録したフックの時間切れの扱いは、コマンドのフックと違う点があります（出典の SDK のページを見てください）。
</Remark>

次のフックは、`.env` という名前のファイルへの書き込みを止めます。入力を読めない場合も止まるように書いてあります。

```python
#!/usr/bin/env python3
# protect_env.py — PreToolUse フック: .env への書き込みを止める。
# 標準入力の JSON を読み、終了コードで答える。
import json, sys

try:
    data = json.load(sys.stdin)
    path = data.get("tool_input", {}).get("file_path", "")
except Exception as e:                    # 入力を読めないときも止める（fail-closed）
    print("hook input unreadable: %s" % e, file=sys.stderr)
    sys.exit(2)
if path.replace("\\", "/").split("/")[-1] == ".env":
    print(".env は編集できません（protect_env フック）", file=sys.stderr)
    sys.exit(2)                           # 2 = ブロックするエラー
sys.exit(0)                               # 0 = 成功。呼び出しは権限の判定へ進む
```

登録する設定は、公式ドキュメントの書式では次の形です（プロジェクトの `.claude/settings.json`）。

```json
{
  "hooks": {
    "PreToolUse": [
      {
        "matcher": "Edit|Write",
        "hooks": [
          {
            "type": "command",
            "command": "${CLAUDE_PROJECT_DIR}/.claude/hooks/protect_env.py"
          }
        ]
      }
    ]
  }
}
```

このフックを Claude Code に登録して動かすことは、この記事ではしていません。代わりに、フックのスクリプトそのものに、`PreToolUse` の入力の形の JSON を標準入力で渡して実行し、<Ref to="def-hook-outcome" /> の規則を表にした模型と合わせて確かめます。

```python
# hook_sim.py — フックの終了コードと決定の合成の模型
# （公式ドキュメントの規則を表にしたもの。実物の実装の再現ではない）
import json, subprocess, sys

def run_hook(tool_name, tool_input):
    payload = {"session_id": "s1", "cwd": "/repo", "hook_event_name": "PreToolUse",
               "tool_name": tool_name, "tool_input": tool_input}
    p = subprocess.run([sys.executable, "protect_env.py"], input=json.dumps(payload),
                       stdout=subprocess.PIPE, stderr=subprocess.PIPE, universal_newlines=True)
    return p.returncode, p.stderr.strip()

def outcome(code, on_failure="continue"):
    if code == 0: return "成功 → 権限の判定へ"
    if code == 2: return "ブロック → 実行されない"
    return "ブロック → 実行されない" if on_failure == "block" else "ブロックしないエラー → 権限の判定へ"

for name, inp in [("Write", {"file_path": "/repo/src/app.py"}), ("Write", {"file_path": "/repo/.env"})]:
    code, err = run_hook(name, inp)
    print(name, inp["file_path"], "終了コード", code, "|", outcome(code), "|", err)
for of in ("continue", "block"):
    print("異常終了（終了コード 1）, onFailure =", of, "|", outcome(1, of))

RANK = {"deny": 3, "defer": 2, "ask": 1, "allow": 0}        # deny > defer > ask > allow
def combine(decisions):
    return max(decisions, key=RANK.get) if decisions else None
def final(hook_decisions, rule):
    """rule: 権限規則の判定（deny / ask / allow / None）。フックの allow は deny・ask の規則を越えない。"""
    h = combine(hook_decisions)
    if h == "deny" or rule == "deny": return "deny"
    if h == "defer": return "defer"
    if h == "ask" or rule == "ask": return "ask"
    if h == "allow" or rule == "allow": return "allow"
    return "モードの既定"
print("合成:", combine(["allow", "ask"]), combine(["allow", "deny", "ask"]), combine(["allow", "allow"]))
for hooks, rule in [(["allow"], "deny"), (["allow"], "ask"), (["deny"], "allow"), (["allow"], None), ([], "allow")]:
    print("フック", hooks, "規則", rule, "->", final(hooks, rule))
```

実行した出力（Python 3.9、2026-10-10 実行）:

```text
Write /repo/src/app.py 終了コード 0 | 成功 → 権限の判定へ | 
Write /repo/.env 終了コード 2 | ブロック → 実行されない | .env は編集できません（protect_env フック）
異常終了（終了コード 1）, onFailure = continue | ブロックしないエラー → 権限の判定へ
異常終了（終了コード 1）, onFailure = block | ブロック → 実行されない
合成: ask deny allow
フック ['allow'] 規則 deny -> deny
フック ['allow'] 規則 ask -> ask
フック ['deny'] 規則 allow -> deny
フック ['allow'] 規則 None -> allow
フック [] 規則 allow -> allow
```

### 4.3. フックと指示ファイルの違い

<Proposition id="prop-hook-determinism" title="フックの実行はモデルの出力に依らない">
[第 1 章](/computer-science/claude-code-internals/agent-loop) のエージェントループの模型に、「ツール呼び出し $(n, x)$ を実行する前に、マッチャが $n$ にマッチするすべての `PreToolUse` フックを $(n, x)$ を入力として実行し、<Ref to="def-hook-outcome" /> の規則で止めるかどうかを決める」という段階を加える。フックは有効で、登録されているとする。

1. モデルがどんな写像であっても、実行されるすべてのツール呼び出し $(n, x)$ について、その実行の前に、マッチするフックが $(n, x)$ を入力として実行されている。
2. 文脈に入っているどんな指示の文 $I$ についても、$I$ に従わないモデル（写像）が存在する。したがって、「$I$ が文脈に入っている」ことから「$I$ の内容が守られる」ことは、模型の上では導けない。
</Proposition>

<Proof of="prop-hook-determinism">
1: 加えた段階は、ハーネスの手順の一部で、ツールの実行の直前に置かれています。模型では、ツールが実行されるのはこの手順を通ったときだけです。手順の中でフックを実行するかどうかは、ツール名 $n$ とマッチャだけで決まり、モデルの出力のうち関係するのは呼び出し $(n, x)$ そのものだけです。よって、モデルがどんな写像でも、実行された呼び出しの前には、マッチするフックの実行があります。これは実行の列についての不変条件で、各段がそれを保つことから従います（<Ref to="computer-science/claude-code-internals/agent-loop#prop-tool-result-pairing" /> の証明と同じ型の帰納法）。

2: 模型ではモデルは任意の写像です。入力のメッセージ列に依らず同じ応答（たとえば、$I$ が禁じる呼び出し 1 つ）を返す定数写像は、モデルとして許されます。この写像は $I$ が文脈にあっても無くても同じ出力を返すので、$I$ に従いません。
</Proof>

2 は「実際のモデルが指示に従わない」という主張ではありません。指示に従うかどうかがモデルという写像の性質であり、ハーネスの構造からは保証できない、という主張です。公式ドキュメントは同じ区別を述べています。フックは決まったイベントで必ず発火し、スキルや CLAUDE.md の指示はモデルが解釈するので結果が変わりうる。「`.env` を編集しない」という指示は依頼であって保証ではなく、編集を止める `PreToolUse` フックが強制である。毎回必ず成り立たせたい決まりは、プロンプトの指示ではなくフックにする。方針や慣習は指示ファイルに書く。この使い分けの根拠が、この命題です。

1 にも、公式ドキュメントが明記する範囲があります。特定の組み込みのツール呼び出し 1 種類はフックを通らないこと、SDK ではターンの上限に達したときフックが発火しないことがあること、などです。また `PreToolUse` が発火するのはモデルがツールを呼んだときだけです。利用者が入力の中でファイルを参照して内容を文脈に入れる操作はツール呼び出しではないので、`Read` にマッチするフックは発火しない、と公式ドキュメントは注意しています（その経路を止めるのは `Read` の deny 規則です）。命題の「フックは有効で、登録されている」という仮定は、<Ref to="rem-hook-exceptions" /> の事情を除くためのものです。

## 5. スキル：必要なときに足される指示

<Definition id="def-skill" title="スキル">
**スキル**とは、`SKILL.md` というファイルを持つディレクトリで、ファイルは YAML の frontmatter（いつ使うかの説明など）と Markdown の本文（指示）からなる。モデルが関連すると判断したとき、または利用者が `/<スキル名>` と入力したときに呼び出される。スキルの**説明**は会話の初めから文脈に置かれ、**本文**は呼び出されたときにメッセージとして文脈に入る。
</Definition>

公式ドキュメント（"Extend Claude with skills"、2026-10-10 確認）から、仕組みに関わる事実を挙げます。

- **置き場所と同名のときの優先**: 組織の管理設定、利用者（`~/.claude/skills/<名前>/SKILL.md`）、プロジェクト（`.claude/skills/<名前>/SKILL.md`）、プラグイン。同じ名前なら、組織のものが利用者のものに、利用者のものがプロジェクトのものに勝ちます。プラグインのスキルは `/<プラグイン名>:<スキル名>` という名前空間つきの名前になります。
- **コマンドとの関係**: 独自のコマンド（`.claude/commands/` のファイル）はスキルに統合されています。`.claude/commands/deploy.md` と `.claude/skills/deploy/SKILL.md` は、どちらも `/deploy` を作ります。同じ名前ならスキルが使われます。
- **説明の長さ**: 一覧に載る説明（`description` と `when_to_use` を合わせたもの）は 1,536 文字で切られます。
- **本文の扱い**: 呼び出されると、本文は 1 つのメッセージとして会話に入り、以後のターンでも残ります。ファイルは以後のターンで読み直されません。
- **[圧縮](/computer-science/claude-code-internals/compaction-memory-subagents#def-compaction)のあと**: 呼び出したスキルのそれぞれについて、最新の呼び出しの先頭 5,000 トークンが要約のあとに再び添付されます。再添付の合計の予算は 25,000 トークンで、新しく呼び出したものから埋めるので、多くのスキルを呼び出したセッションでは古いものが落ちます。
- **欄**: frontmatter の欄として、モデルによる自動の呼び出しを止めるもの（`disable-model-invocation`。このとき説明も文脈に置かれない）、スキルを呼び出したターンで確認なしに使えるツールを指定するもの（`allowed-tools`）、サブエージェントの中で実行するもの（`context: fork`）などがあります。本文には、引数の置換（`$ARGUMENTS`）や、シェルコマンドの出力を本文に埋め込む記法があります。

最小のスキルは、次のようなファイルです（`.claude/skills/summarize-diff/SKILL.md`。内容は自作の例）。

```markdown
---
description: 未コミットの変更を要約し、危険そうな箇所を挙げる。差分の確認やコミットメッセージの相談のときに使う。
---

## 手順

1. `git diff HEAD` の結果を読む。
2. 変更点を 3 行以内で要約する。
3. エラー処理の抜け、値の決め打ち、更新が必要なテストがあれば挙げる。
4. 差分が空なら、未コミットの変更は無いと答える。
```

<Proposition id="prop-skill-context-cost" title="スキルの文脈コスト">
モデルが自分で呼び出せるスキルが $n$ 個あり、スキル $i$ の（切られたあとの）説明の長さを $\ell_i \le \ell$、本文の長さを $b_i$ とする。

1. どのスキルも呼び出していないとき、スキルのために文脈に置かれる量は $\sum_i \ell_i \le n\,\ell$ で、本文の長さ $b_1, \ldots, b_n$ に依らない。
2. 圧縮のあとに再び添付される量は、スキル 1 個あたりの上限を $a$、合計の上限を $A$、呼び出した相異なるスキルの数を $k$ とすると、高々 $\min(k\,a,\ A)$。
</Proposition>

<Proof of="prop-skill-context-cost">
1: 呼び出していないスキルについて文脈に置かれるのは説明だけなので、量は $\sum_i \ell_i$ です。各 $\ell_i \le \ell$ なので $n\,\ell$ 以下です。この式に $b_i$ は現れません。

2: 再添付はスキルごとに高々 $a$ で、対象は $k$ 個なので合計は高々 $k\,a$ です。合計には上限 $A$ もあるので、高々 $A$ でもあります。両方が成り立つので、$\min(k\,a, A)$ 以下です。
</Proof>

公開されている値は $a = 5{,}000$ トークン、$A = 25{,}000$ トークンです（2026-10 時点）。$k = 3$ なら上界は $15{,}000$、$k \ge 5$ なら $25{,}000$ です。説明の上限は文字数（1,536 文字）で定められているので、トークン数での $\ell$ はトークンへの分割に依ります。公式ドキュメントによれば、一覧の全体にも文字数の予算があり（既定でモデルのコンテキストウィンドウの 1% に比例。設定で変更できる）、一覧がこれを超えると、名前はすべて残したまま、呼び出しの少ないスキルから説明が省かれます。1 の $\sum_i \ell_i$ は、この予算を適用したあとに実際に置かれた説明の長さの和と読んでください（上界 $n\,\ell$ は変わりません）。

1 は、スキルが CLAUDE.md と違う点です。CLAUDE.md は全文が毎回の要求に入るので、長い手順書を書くほど常駐のコストが増えます。スキルにすれば、常駐するのは説明だけです。前節の <Ref to="prop-deferred-tool-cost" /> と同じ構造（名前や説明だけを置き、本体は使うときに読む）で、どちらも[コンテキストウィンドウ](/computer-science/claude-code-internals/context-window-and-prompt-caching#def-context-window)の節約です。スキルの本文は会話のメッセージとして末尾に足されるので、読み込みは既存のキャッシュを無効にしません（<Ref to="prop-deferred-tool-cost" /> の 4 と同じ理由）。

## 6. プラグインと、設定の出所の優先順位

<Definition id="def-plugin" title="プラグインとマーケットプレイス">
**プラグイン**とは、スキル・エージェント・フック・MCP サーバなどの部品を入れたディレクトリで、Claude Code が 1 つの単位として導入し読み込むものをいう。名前などを書くマニフェストは `.claude-plugin/plugin.json` に置く。**マーケットプレイス**とは、`.claude-plugin/marketplace.json` を持つリポジトリまたはディレクトリで、プラグインとその取得元の一覧（カタログ）である。
</Definition>

公式ドキュメント（"Plugins overview"、2026-10-10 確認）は、有効なプラグインがセッションに足すものを 3 つ挙げています。モデルが自分で呼び出せるスキル・エージェント・コマンドの名前と説明は、何も使わないセッションでも毎ターンの文脈に入る（本文は使ったときだけ読み込まれる）。プラグインが定義する MCP サーバはセッションと並んで動き、フックはそのイベントで発火する。プラグインが実行するものは、利用者の権限で動く。導入の範囲は、利用者・プロジェクト・ローカルの 3 つです。

同じ名前の定義が複数の出所にあるときの扱いは、仕組みごとに公開されています。

| 仕組み | 同名のときの扱い（公式ドキュメントによる） |
|---|---|
| MCP サーバ | 優先度の高い出所の定義を使う。順は local、project、user、プラグイン提供、claude.ai のコネクタ。**その出所のエントリ全体**が使われ、欄ごとの併合はしない |
| スキル | 組織、利用者、プロジェクトの順で 1 つが勝つ。プラグインのものは名前空間で分かれる |
| サブエージェント | 組織の管理設定、コマンドラインの指定、プロジェクト、利用者、プラグインの順で 1 つが勝つ |
| フック | 併合する。登録されたものは、出所に依らずすべて発火する |
| CLAUDE.md | 加法的。すべての階層の内容が文脈に入る |

次の命題のねらいは、同じ名前の定義が複数の出所にあっても、優先度の順に最上位の出所を採る規則なら、使われる定義が毎回ただ 1 つに決まることを確かめることです。実用上は、どの出所を見れば今の定義が分かるかがはっきりします（直後の例で具体的に見ます）。

<Proposition id="prop-precedence-unique" title="最上位の出所を採る規則は、定義を一意に決める">
出所の有限集合 $\Sigma$ に全順序（優先度）が定められ、各出所 $\sigma$ は、名前から定義への部分写像 $\mathrm{def}_\sigma$ を与えるとする。名前 $\nu$ を定義する出所の集合 $\Sigma_\nu = \{\sigma : \mathrm{def}_\sigma(\nu) \text{ が定義されている}\}$ が空でないとき、「$\Sigma_\nu$ の中で優先度が最大の出所 $\sigma^{\ast}$ の定義 $\mathrm{def}_{\sigma^{\ast}}(\nu)$ を採る」という規則は、採用される定義を一意に決める。採用された定義は、ただ 1 つの出所の定義にそのまま等しい。
</Proposition>

<Proof of="prop-precedence-unique">
$\Sigma_\nu$ は有限で空でない全順序集合なので、最大元がちょうど 1 つあります（有限個の元を 2 つずつ比べて大きいほうを残していけば 1 つが残り、全順序なので、それより大きい元はありません。最大元が 2 つあれば互いに相手以上なので等しくなります）。$\sigma^{\ast}$ が一意なので、$\mathrm{def}_{\sigma^{\ast}}(\nu)$ も一意です。規則はこの値をそのまま採るので、採用された定義は出所 $\sigma^{\ast}$ の定義に等しくなります。
</Proof>

<Example id="ex-whole-entry-vs-merge" title="エントリ全体を採る方式と、欄ごとに併合する方式">
MCP サーバ `db` が、project の範囲では「URL は `https://db.example.com/mcp`、ヘッダに認証の値」、local の範囲では「URL は `http://localhost:9000/mcp`」と定義されているとします（自作の例）。

- **エントリ全体を採る方式**（公開されている MCP の規則）: local が勝ち、使われる定義は「URL は `http://localhost:9000/mcp`」だけです。ヘッダは付きません。使われている定義を知るには、local の定義だけを見れば足ります。
- **欄ごとに併合する方式**（比較のための仮の方式）: URL は local のもの、ヘッダは project のものになり、project の認証の値が local の URL に送られます。どの欄がどの出所から来たかは、欄ごとに違いえます。出所が $k$ 個、欄が $f$ 個なら、欄と出所の組み合わせは最大で $k^f$ 通りあり、使われている定義を知るにはすべての出所を見る必要があります。

エントリ全体を採る方式では、<Ref to="prop-precedence-unique" /> のとおり、採用された定義が 1 つの出所の定義に等しいので、このような「どの出所も書いていない組み合わせ」は生じません。
</Example>

フックが併合であること（すべて発火する）は、<Ref to="prop-hook-exit-semantics" /> の 1 と合わせると、安全側に働きます。どの出所のフックがブロックしても呼び出しは止まるので、フックを足しても、止められる呼び出しの集合は減りません。権限規則で deny を足しても許可される集合が増えないこと（<Ref to="computer-science/claude-code-internals/tools-and-permissions#prop-deny-monotone" />）と同じ向きの性質です。

## 7. Agent SDK：ループを自分のプログラムから動かす

Agent SDK は、Claude Code と同じ実行ループを、自分のプログラムから動かすためのライブラリです（公式ドキュメント "How the agent loop works" による）。プログラムはプロンプトと選択肢（オプション）を渡し、ループが出すメッセージの列を受け取ります。選択肢は、第 1 章の模型の要素に対応します。

| SDK の選択肢（Python の名前） | 模型の要素 |
|---|---|
| `prompt` | 最初の user メッセージ |
| `allowed_tools`、`disallowed_tools`、`permission_mode` | 関門（実行するか拒否するかを決める写像） |
| `hooks`（`PreToolUse` など） | 関門の一部（実行前）、および実行後の処理 |
| `mcp_servers` | ツール定義の集合と、各ツールの実行（MCP サーバのツールの分） |
| `max_turns` | ツール使用ターンの上限 $T$ |
| `max_budget_usd` | 支出の上限 |
| `agents` | サブエージェント（別のループ） |
| `setting_sources` | どの設定ファイルと指示ファイルを読み込むか（固定部分） |

<Proposition id="prop-sdk-same-loop" title="SDK のループへの停止性の適用">
Agent SDK で組んだループが、上の対応表のとおりに [第 1 章](/computer-science/claude-code-internals/agent-loop) の模型の実例であるとする。`max_turns` を有限の値 $T$ に設定すると、そのループのモデルの呼び出しは高々 $T + 1$ 回で、ループは停止する。`max_turns` を設定しない（上限なし）場合、模型の上では停止は保証されない。
</Proposition>

<Proof of="prop-sdk-same-loop">
仮定により、このループは第 1 章の模型で、関門・ツールの実行・上限を対応表のとおりに定めたものです。第 1 章の停止性の定理（<Ref to="computer-science/claude-code-internals/agent-loop#thm-loop-termination" />）は、モデル・関門・ツールがどんな写像であっても、上限 $T$ が有限ならモデルの呼び出しは高々 $T + 1$ 回で停止する、と述べています。フックや権限の設定は関門の定め方を変えるだけなので、定理の仮定は満たされたままです。上限が無い場合に停止が保証されないことも、同じ定理（常にツールを呼ぶモデルという反例）によります。
</Proof>

公式ドキュメントは、`max_turns` がツール使用ターンだけを数える上限であること、既定では上限が無いことを述べています。入力を流し込む使い方では、待ち行列に残っていたメッセージに対して新しいターンが始まり、数え直しになる、とも書かれているので、この上界は「1 つの入力に対する 1 回のループ」についてのものです。サブエージェントは別のループなので、その中のモデルの呼び出しはこの上限には数えられません（前章の入れ子と同時実行の上限、および支出の上限が、そちらの歯止めになります）。

次のコードは、自作のツールとフックを渡してループを動かす最小の形で、公式ドキュメント（"Give Claude custom tools" と "Intercept and control agent behavior with hooks"、2026-10-10 確認）の書式に合わせています。SDK の中のサーバは、プログラムと同じプロセスの中で動く MCP サーバです。**このコードは、この記事では実行していません**（API を呼ぶため。出力も載せません）。API キーは環境変数から読まれます。

```python
# 未実行。Agent SDK の公式ドキュメントの書式（2026-10 時点）に合わせた最小の形。
import asyncio
from typing import Any
from claude_agent_sdk import (
    ClaudeSDKClient, ClaudeAgentOptions, HookMatcher, ResultMessage,
    tool, create_sdk_mcp_server,
)


@tool("add", "2 つの数の和を返す", {"a": float, "b": float})
async def add(args: dict[str, Any]) -> dict[str, Any]:
    return {"content": [{"type": "text", "text": str(args["a"] + args["b"])}]}


calc = create_sdk_mcp_server(name="calc", version="0.1.0", tools=[add])


async def protect_env(input_data, tool_use_id, context):   # PreToolUse のコールバック
    path = input_data["tool_input"].get("file_path", "")
    if path.split("/")[-1] == ".env":
        return {"hookSpecificOutput": {
            "hookEventName": input_data["hook_event_name"],
            "permissionDecision": "deny",
            "permissionDecisionReason": ".env は編集できません",
        }}
    return {}                                               # 何も決めない


async def main():
    options = ClaudeAgentOptions(
        mcp_servers={"calc": calc},
        allowed_tools=["mcp__calc__add"],                   # mcp__<サーバ名>__<ツール名>
        hooks={"PreToolUse": [HookMatcher(matcher="Write|Edit", hooks=[protect_env])]},
        max_turns=5,                                        # ツール使用ターンの上限 T
    )
    async with ClaudeSDKClient(options=options) as client:
        await client.query("add ツールを使って 2 と 3 の和を求めて")
        async for message in client.receive_response():
            if isinstance(message, ResultMessage):
                print(message.subtype, message.result)


asyncio.run(main())
```

MCP の公式の SDK（言語ごとのライブラリ）を使ってサーバを書く方法もありますが、この記事では扱っていません（3.4 節のサーバは標準ライブラリだけで書いたものです）。

## 8. まとめ：どこに何を差し込めるか

| 仕組み | 実行の主体 | モデルの出力に依る部分 | この章の命題 |
|---|---|---|---|
| MCP | サーバ（外のプログラム） | どのツールをいつ呼ぶか | <Ref to="prop-jsonrpc-correlation" />、<Ref to="prop-version-negotiation" />、<Ref to="prop-two-error-channels" />、<Ref to="prop-deferred-tool-cost" /> |
| フック | ハーネス | 無い（イベントの発生はループの構造で決まる） | <Ref to="prop-hook-exit-semantics" />、<Ref to="prop-hook-determinism" /> |
| スキル | モデル（指示を読んで従う） | いつ呼び出すか、指示に従うか | <Ref to="prop-skill-context-cost" /> |
| プラグイン | 上の組み合わせ | 部品による | <Ref to="prop-precedence-unique" /> |
| Agent SDK | 自分のプログラム | ループの中は同じ | <Ref to="prop-sdk-same-loop" /> |

拡張を 1 つ足すと、「外から文脈に入る内容の出所」か「利用者の権限で自動的に動くプログラム」が 1 つ増えます。MCP サーバはその両方で、サーバのプロセスは利用者の機械の上で動き、ツールの説明と結果はモデルの文脈に入ります。[次章](/computer-science/claude-code-internals/safety-and-trust-boundaries) では、この一覧をもとに、どこに信頼の境界があるかを調べます。

## 9. 演習

<Exercise id="exr-negotiation-cases" difficulty="標準">
版 $v_1 \lt v_2 \lt v_3 \lt v_4$ について、次の各場合に、握手による方式（双方が最新の版を出す）と、要求ごとに版を示す方式のそれぞれで、取り決めが成立するかを答えよ。(a) $C = \{v_1, v_2, v_3\}$、$S = \{v_2, v_3, v_4\}$。(b) $C = \{v_1, v_4\}$、$S = \{v_1, v_2, v_3\}$。(c) $C = \{v_1, v_2\}$、$S = \{v_3, v_4\}$。
<Solution>
(a) 握手: $\max C = v_3 \in S$ なので成立します（<Ref to="prop-version-negotiation" /> の 1）。$C$ も $S$ も区間で共通部分が空でないので、3 からも分かります。要求ごと: $C \cap S = \{v_2, v_3\} \ne \varnothing$ なので成立します。

(b) 握手: $\max C = v_4 \notin S$。サーバは $\max S = v_3$ を返し、$v_3 \notin C$ なので成立しません。$C$ が区間でないので 3 は使えません。要求ごと: $C \cap S = \{v_1\} \ne \varnothing$ なので、高々 2 回の要求で成立します。

(c) $C \cap S = \varnothing$ なので、どちらの方式でも成立しません。
</Solution>
</Exercise>

<Exercise id="exr-id-reuse" difficulty="易">
クライアントが、id が $7$ の要求を送り、その応答を受け取ったあとで、再び id が $7$ の要求を送った。これは (a) 2025-11-25 版、(b) 2026-07-28 版の要請に反するか。また、<Ref to="prop-jsonrpc-correlation" /> の仮定は満たされているか。
<Solution>
(a) 反します。2025-11-25 版は、同じセッションの中で以前に使った ID の再利用を禁じています。(b) 反しません。2026-07-28 版が禁じるのは、まだ応答を受け取っていない他の要求と ID が一致することで、最初の要求にはすでに応答が届いています。命題の仮定（未応答の要求の集合の上で id が単射）は、2 つ目の要求を送った時点で未応答の要求が 1 つだけなので満たされていて、応答の対応は一意に決まります。
</Solution>
</Exercise>

<Exercise id="exr-fail-open" difficulty="標準">
`PreToolUse` のフックのスクリプトが、設定に書かれたパスに存在しない（起動できない）とする。(1) `onFailure` が既定のとき、(2) `"block"` のとき、マッチするツール呼び出しはどうなるか。「秘密のファイルへの書き込みを止める」ためのフックと、「編集のあとで整形する」ためのフックのそれぞれについて、どちらの設定が合うかを理由とともに答えよ。
<Solution>
起動できないのは失敗（ブロックしないエラー）です。(1) 既定（`"continue"`）では行為は進み、呼び出しは権限の判定へ進みます。(2) `"block"` では呼び出しは実行されません（<Ref to="prop-hook-exit-semantics" /> の 2）。

書き込みを止めるフックは、止めることが目的です。(1) だと、パスの間違い 1 つで保護が黙って無くなるので、(2) の fail-closed が合います。整形のフックは実行後の付随的な処理で、失敗しても作業を止める理由がありません。なお、整形は `PostToolUse` で行うのが普通で、このイベントはツールの実行のあとなので、そもそもブロックできません。
</Solution>
</Exercise>

<Exercise id="exr-skill-budget" difficulty="易">
1 つのセッションで 7 個のスキルを呼び出し、本文はどれも 6,000 トークンだったとする。公開されている上限（1 個あたり 5,000 トークン、合計 25,000 トークン）のもとで、圧縮のあとに再び添付される量の上界と、内容がまったく添付されないスキルが少なくとも何個あるかを求めよ。
<Solution>
上界は $\min(7 \times 5{,}000,\ 25{,}000) = 25{,}000$ トークンです（<Ref to="prop-skill-context-cost" /> の 2）。各スキルは 5,000 トークンに切られ、新しく呼び出したものから予算を埋めるので、$25{,}000 / 5{,}000 = 5$ 個で予算が尽きます。残りの $7 - 5 = 2$ 個（古いもの）は添付されません。切るときはファイルの先頭が残るので、大事な指示は本文の初めに書くのがよい、と公式ドキュメントは勧めています。
</Solution>
</Exercise>

## 参考文献

いずれも公式の一次資料で、確認日は 2026-10-10 です。MCP の仕様は版ごとに URL が分かれています。

- Model Context Protocol, "Versioning"、および仕様 2026-07-28 版の "Overview"（Base Protocol）・"Versioning and Compatibility"・"Discovery"・"stdio"・"Tools"・"Key Changes"。[https://modelcontextprotocol.io/specification/versioning](https://modelcontextprotocol.io/specification/versioning)、[https://modelcontextprotocol.io/specification/2026-07-28/basic](https://modelcontextprotocol.io/specification/2026-07-28/basic)
- Model Context Protocol, 仕様 2025-11-25 版の "Overview"・"Lifecycle"（握手による方式の比較のため）。[https://modelcontextprotocol.io/specification/2025-11-25/basic/lifecycle](https://modelcontextprotocol.io/specification/2025-11-25/basic/lifecycle)
- Model Context Protocol, "Architecture overview"。[https://modelcontextprotocol.io/docs/learn/architecture](https://modelcontextprotocol.io/docs/learn/architecture)
- JSON-RPC Working Group, "JSON-RPC 2.0 Specification"。[https://www.jsonrpc.org/specification](https://www.jsonrpc.org/specification)
- Anthropic, Claude Code ドキュメントの "Extend Claude Code"・"Connect Claude Code to tools via MCP"・"Hooks reference"・"Extend Claude with skills"・"Plugins overview"・"Configure permissions"。[https://code.claude.com/docs/en/features-overview](https://code.claude.com/docs/en/features-overview)、[https://code.claude.com/docs/en/mcp](https://code.claude.com/docs/en/mcp)、[https://code.claude.com/docs/en/hooks](https://code.claude.com/docs/en/hooks)、[https://code.claude.com/docs/en/skills](https://code.claude.com/docs/en/skills)、[https://code.claude.com/docs/en/plugins/overview](https://code.claude.com/docs/en/plugins/overview)
- Anthropic, Agent SDK ドキュメントの "How the agent loop works"・"Intercept and control agent behavior with hooks"・"Give Claude custom tools"。[https://code.claude.com/docs/en/agent-sdk/agent-loop](https://code.claude.com/docs/en/agent-sdk/agent-loop)、[https://code.claude.com/docs/en/agent-sdk/hooks](https://code.claude.com/docs/en/agent-sdk/hooks)、[https://code.claude.com/docs/en/agent-sdk/custom-tools](https://code.claude.com/docs/en/agent-sdk/custom-tools)
