コントロールプロトコル
CLIとSDK間の双方向JSON RPCを理解する。
学ぶこと
Claude Codeは単一のプロセスではありません。明確に定義されたプロトコルを通じて連携する2つのプロセスです。この分離と、2つの間を流れるメッセージを理解することで、カスタムフロントエンドの構築、ワークフローの自動化、予期しない動作のデバッグが可能になります。
このセッションを終えると、以下がわかるようになります:
- Claude CodeがCLIとSDKプロセスに分離されている理由
- 双方向JSON-RPCメッセージがstdin/stdout上でどのように流れるか
- 3つのメッセージカテゴリ:リクエスト、レスポンス、通知
- プロトコルレベルでのストリーミングの仕組み
- パーミッションプロンプトがプロトコルを通じてどのように伝達されるか
- 単一のユーザーメッセージが完全なラウンドトリップを通過する流れ
課題
ほとんどのCLIツールはモノリシックです。1つのプロセスが入力を読み取り、処理を行い、出力を表示します。しかし、Claude Codeには2つの全く異なる仕事があります:
- ユーザーインターフェース — ターミナルUIのレンダリング、キーボード入力の処理、ストリーミングテキストの表示
- AIエンジン — APIコールの管理、ツールの実行、パーミッションの適用、コンテキストの追跡
両方を1つのプロセスに詰め込むと、密結合が生まれます。Claude CodeをVS Codeに組み込みたい場合、UIレイヤーを書き直す必要があります。スクリプトから駆動したい場合、ターミナル入力をシミュレートする必要があります。エンジンをリモートサーバーで実行したい場合、不可能です。
解決策はクリーンな分離です:
┌──────────────────┐ ┌──────────────────┐
│ CLI │ stdin │ SDK │
│ │ ──────► │ │
│ Terminal UI │ │ API Client │
│ User input │ stdout │ Tool Executor │
│ Permission UI │ ◄────── │ Context Manager │
│ Display │ │ Permission Logic │
└──────────────────┘ └──────────────────┘
CLIはユーザーが目にするすべてを処理します。SDKはAIが行うすべてを処理します。両者はコントロールプロトコルを通じて通信します。これはstdinとstdout上の双方向JSON-RPCストリームです。
仕組み
2プロセスアーキテクチャ
claudeを起動すると、2つのプロセスが開始されます:
Terminal
│
▼
┌──────────────────────────────────────────────┐
│ CLI Process (Node.js) │
│ │
│ - Renders terminal UI (Ink/React) │
│ - Captures user keystrokes │
│ - Displays streaming assistant text │
│ - Shows permission prompts │
│ - Manages session history display │
│ │
│ stdin (pipe) ▼ ▲ stdout (pipe) │
│ │
│ ┌────────────────────────────────────────┐ │
│ │ SDK Process (Node.js, child process) │ │
│ │ │ │
│ │ - Sends requests to Claude API │ │
│ │ - Parses streaming API responses │ │
│ │ - Executes tool calls (Read, Edit, │ │
│ │ Bash, etc.) │ │
│ │ - Manages message history │ │
│ │ - Resolves permissions │ │
│ │ - Handles context compaction │ │
│ └────────────────────────────────────────┘ │
└──────────────────────────────────────────────┘
SDKはCLIの子プロセスとして実行されます。両者は子プロセスのstdinとstdoutパイプを介して通信します。ネットワークソケットもHTTPもなく、改行区切りのJSONメッセージを使った純粋なパイプ通信です。
メッセージタイプ
プロトコルは、JSON-RPC 2.0に着想を得た3つのメッセージカテゴリを使用します:
1. リクエスト — レスポンスを期待します。idフィールドを持ちます。
{
"jsonrpc": "2.0",
"id": 1,
"method": "user_message",
"params": {
"content": "Fix the bug in auth.ts",
"session_id": "abc-123"
}
}
2. レスポンス — リクエストに応答します。一致するidを持ちます。
{
"jsonrpc": "2.0",
"id": 1,
"result": {
"status": "completed",
"usage": { "input_tokens": 1200, "output_tokens": 350 }
}
}
3. 通知 — ファイアアンドフォーゲット。idフィールドはありません。
{
"jsonrpc": "2.0",
"method": "assistant_token",
"params": {
"token": "Let me ",
"message_id": "msg-456"
}
}
この区別は重要です。リクエストは会話を作成し(一方がレスポンスを待つ)、通知は一方向のブロードキャストです。
ストリーミングモデル
SDKがAPIレスポンスを受信すると、完全なレスポンスを待ちません。各トークンが通知としてストリーミングされます:
SDK → CLI: {"method": "assistant_token", "params": {"token": "Let "}}
SDK → CLI: {"method": "assistant_token", "params": {"token": "me "}}
SDK → CLI: {"method": "assistant_token", "params": {"token": "check "}}
SDK → CLI: {"method": "assistant_token", "params": {"token": "the "}}
SDK → CLI: {"method": "assistant_token", "params": {"token": "file."}}
SDK → CLI: {"method": "tool_use_start", "params": {"name": "Read", ...}}
CLIは各トークンを即座にレンダリングし、ターミナルで目にするタイプライター効果を生み出します。これはServer-Sent Events(SSE)に類似しており、小さなメッセージのストリームが一方向に流れます。
Claude API (SSE) SDK Process CLI Process
│ │ │
│── token: "Let " ────►│ │
│ │── notification ──►│── render "Let "
│── token: "me " ─────►│ │
│ │── notification ──►│── render "me "
│── tool_use ─────────►│ │
│ │── tool_start ────►│── show tool badge
│ │ │
│ │── (execute tool) ──│
│ │ │
│ │── tool_result ───►│── show result
パーミッションフロー
SDKがユーザーの承認が必要なツールコールに遭遇すると、リクエスト-レスポンスの交換で処理されます:
SDK → CLI: {
"jsonrpc": "2.0",
"id": 42,
"method": "permission_request",
"params": {
"tool": "Bash",
"command": "npm install express",
"risk_level": "medium"
}
}
(CLIがユーザーにパーミッションプロンプトを表示)
(ユーザーが 'y' を押す)
CLI → SDK: {
"jsonrpc": "2.0",
"id": 42,
"result": {
"decision": "allow",
"scope": "session"
}
}
SDKはこのリクエストでブロックします。CLIがレスポンスを返すまでツールを実行しません。これがパーミッションシステムの仕組みです。SDKはパーミッションが必要かどうかの権限を持ち、CLIはそれを許可するかどうかの権限を持ちます。
SDK CLI
│ │
│── permission_request ─────►│
│ (blocked) │── Show prompt
│ (waiting) │── User decides
│◄── permission_response ────│
│ │
│── execute tool │
│── tool_result ────────────►│── Display result
プロトコルバージョニング
最初に交換されるメッセージは、常にケーパビリティネゴシエーションです:
CLI → SDK: {
"jsonrpc": "2.0",
"id": 0,
"method": "initialize",
"params": {
"protocol_version": "1.0",
"capabilities": {
"streaming": true,
"permissions": true,
"progress": true
}
}
}
SDK → CLI: {
"jsonrpc": "2.0",
"id": 0,
"result": {
"protocol_version": "1.0",
"capabilities": {
"tools": ["Read", "Edit", "Bash", "Glob", "Grep", ...],
"mcp_servers": 3
}
}
}
このハンドシェイクにより、両側がサポートする機能について合意できます。CLIが古いバージョンの場合、SDKは新しい機能を無効にできます。SDKが新しい通知タイプを導入した場合、古いCLIはそれを安全に無視できます。
重要なポイント
コントロールプロトコルこそが、Claude CodeをCLIツールとSDKの両方にしているものです。 ターミナルUIを支えるのと同じエンジンを、VS Code、Webインターフェース、またはPythonスクリプトから駆動できます。プロトコルが境界なのです。
これには深い意味があります:
- カスタムフロントエンド。 プロトコルを話すGUIを構築すれば、ターミナルなしでClaude Codeの全機能を得られます。
- プログラムによる自動化。
@anthropic-ai/claude-codenpmパッケージはSDKを直接公開しています。あなたのコードがCLIとなり、メッセージを送信してレスポンスを処理します。 - リモート実行。 プロトコルはトランスポートに依存しません。現在はstdin/stdoutパイプ上で動作していますが、最小限の変更でWebSocketやTCP上でも動作できます。
- テスト。 UIなしで、スクリプト化されたプロトコルメッセージを送信してSDKをテストできます。
プロトコルは実装の詳細ではありません。それがアーキテクチャそのものなのです。
ハンズオン例
メッセージをフルスタックで追跡する
Claude Codeに「What’s in README.md?」と入力したときに何が起こるか追跡してみましょう:
Step 1: CLIが入力をキャプチャ
─────────────────────────
ユーザーが入力: "What's in README.md?"
CLIがJSON-RPCリクエストにパッケージ化。
CLI → SDK:
{ "id": 5, "method": "user_message",
"params": { "content": "What's in README.md?" } }
Step 2: SDKがClaude APIを呼び出し
─────────────────────────────
SDKが完全なメッセージ配列を構築(システムプロンプト + 履歴 + 新メッセージ)
SDKがストリーミングHTTPでClaude APIに送信。
Step 3: APIがトークン + ツールコールをストリーミング返却
───────────────────────────────────────────
APIレスポンスにはテキストとtool_useブロックが含まれる:
"Let me read that file."
tool_use: Read { file_path: "README.md" }
SDKは各トークンを通知として転送:
SDK → CLI: { "method": "assistant_token", "params": { "token": "Let " } }
SDK → CLI: { "method": "assistant_token", "params": { "token": "me " } }
...
SDK → CLI: { "method": "tool_use_start",
"params": { "name": "Read", "input": { "file_path": "README.md" } } }
Step 4: SDKがパーミッションを確認
──────────────────────────────
README.mdのReadツール → デフォルトで許可(プロンプト不要)。
SDKがツールを直接実行。
Step 5: SDKが結果をAPIにフィードバック
─────────────────────────────────────
SDKがtool_resultをメッセージ配列に追加。
SDKが更新されたコンテキストで再度Claude APIを呼び出し。
Step 6: APIが最終レスポンスをストリーミング
───────────────────────────────────
API → SDK → CLI(トークンごと):
"The README.md contains a project description for..."
SDK → CLI: { "method": "turn_complete",
"params": { "stop_reason": "end_turn" } }
Step 7: SDKが元のリクエストにレスポンスを送信
───────────────────────────────────────────────
SDK → CLI:
{ "id": 5, "result": { "status": "completed",
"usage": { "input_tokens": 2400, "output_tokens": 180 } } }
SDKをプログラムで使用する
プロトコルが明確に定義されているため、Claude Codeをライブラリとして使用できます:
import { claude } from "@anthropic-ai/claude-code";
// SDKは同じプロトコルを話す — あなたのコードが「CLI」になる
const result = await claude("What's in README.md?", {
cwd: "/my/project",
allowedTools: ["Read", "Glob", "Grep"],
});
console.log(result.text);
// "The README.md contains..."
内部的には、SDKプロセスを起動し、stdin/stdout上でJSON-RPCメッセージを交換します。ターミナルCLIがするのとまったく同じです。
何が変わったか
| モノリシックCLI | 2プロセスプロトコル |
|---|---|
| UIとエンジンが密結合 | 関心の明確な分離 |
| フロントエンドは1つだけ(ターミナル) | JSON-RPCを話す任意のフロントエンド |
| 自動化が困難 | プログラムからのSDK使用が組み込み |
| テストが困難 | プロトコルメッセージをスクリプト化可能 |
| エンジンをリモート実行不可 | トランスポートに依存しない設計 |
| パーミッションロジックとUIが混在 | SDKがパーミッションの要否を判断、CLIが許可の可否を判断 |
次のセッション
CLIとSDKの通信方法を理解しました。しかし、ツールだけがClaude Codeを拡張する方法ではありません。セッション14ではMCP統合を扱います。Model Context Protocolが標準プロトコルを通じて無限の外部ツールをプラグインする方法を学び、Claude Codeを無限に拡張可能にします。