Claude Media
Agent SDKのHooksでエージェントの挙動を横取りする

Agent SDKのHooksでエージェントの挙動を横取りする

Agent SDKのHooksはoptions.hooksに渡すコールバック関数です。settings.jsonで書くClaude Code CLIのHooksとは実装が別物で、設定方法・入出力・実装例を扱います。

Agent SDKのHooksは、ツール呼び出しやセッション終了などのイベントに反応してコードを走らせる仕組みです。Claude Code CLIのHooksと名前は同じですが、実装はまったく別物です。CLIはシェルコマンドをsettings.jsonに書きますが、SDKはoptions.hooksにコールバック関数を直接渡します。この記事では設定方法・入出力の形・実装例と、CLIのHooksとの違いを扱います。

Agent SDKのHooksとは何か

Agent SDKのHooksとは、ClaudeAgentOptions(Python)やoptionsオブジェクト(TypeScript)のhooksフィールドに渡す、イベント駆動のコールバック関数です。ツール呼び出しの直前・直後、セッションの開始・終了、サブエージェントの起動・終了などのタイミングで自分の関数が呼ばれます。

コールバックは3つの引数を受け取ります。イベントの詳細を持つ入力データ、ツール呼び出しを紐づけるtool use ID、そしてキャンセル用のシグナルを含むコンテキストです。処理を終えたら出力オブジェクトを返し、許可・拒否・入力の書き換え・追加コンテキストの注入のいずれかを指示します。

Claude Code CLIのHooksと何が違うか

同じ「Hooks」という名前でも、CLIとSDKでは設定の置き場所と実行主体が違います。

観点Claude Code CLIAgent SDK
設定場所Claude Code CLI.claude/settings.jsonのJSONAgent SDKoptions.hooksのコード
実行単位Claude Code CLIシェルコマンド・HTTP・MCPツール等5種類Agent SDK言語ネイティブのコールバック関数
入出力の受け渡しClaude Code CLIstdin/stdoutのJSONAgent SDK関数の引数と返り値
対応イベント数Claude Code CLI33種類Agent SDKイベントによりPython/TypeScriptで差あり

Claude Code Hooks完全ガイドで解説したCLIのHooksは、settings.jsonにシェルコマンドを書く形式です。SDKのHooksはこの形式を使いません。関数をそのまま渡すため、シェルを経由するオーバーヘッドがなく、TypeScriptなら型定義の恩恵も受けられます。

一方で、SDKアプリケーションから.claude/settings.jsonのシェルコマンド型Hooksを読み込むこともできます。Pythonのsetting_sources、TypeScriptのsettingSources"project"を指定すると、プロジェクト設定のHooksがSDKセッションでも有効になります。SessionStartSessionEndはPython SDKのコールバックとしては使えず、この経路でしか利用できません。

利用できるイベントの一覧

CLIとほぼ同じ名前のイベント群が用意されています。ツール呼び出しの前後を扱うPreToolUsePostToolUsePostToolUseFailure、プロンプト送信時のUserPromptSubmit、応答終了時のStop、サブエージェントの開始・終了を扱うSubagentStart/SubagentStopが代表例です。ほかにも圧縮前のPreCompact、許可判断が必要なときのPermissionRequest、ステータス通知のNotification、セッションの開始・終了を扱うSessionStart/SessionEndがあります。

TypeScript SDKのほうが対応イベントが多く、PostToolBatchMessageDisplayPreModelSwitchPostModelSwitchInstructionsLoadedElicitation系はTypeScript限定です。Python SDKは主要なツール系・セッション系イベントに絞られています。どちらのSDKを使うかで、書けるHooksの範囲が変わる点は選定時に確認が必要です。

Hooksを設定する — matcherとコールバックの書き方

hooksオプションは、イベント名をキーにした辞書(Python)またはオブジェクト(TypeScript)です。値はHookMatcherの配列で、各要素が対象を絞るmatcherパターンとコールバックの配列を持ちます。

const protectEnvFiles: HookCallback = async (input, toolUseID, { signal }) => {
  const preInput = input as PreToolUseHookInput;
  const toolInput = preInput.tool_input as Record<string, unknown>;
  const filePath = toolInput?.file_path as string;
  const fileName = filePath?.split("/").pop();
 
  if (fileName === ".env") {
    return {
      hookSpecificOutput: {
        hookEventName: preInput.hook_event_name,
        permissionDecision: "deny",
        permissionDecisionReason: "Cannot modify .env files"
      }
    };
  }
  return {};
};
 
const options = {
  hooks: {
    PreToolUse: [{ matcher: "Write|Edit", hooks: [protectEnvFiles] }]
  }
};

matcherはツール名に対するパターンです。"Write|Edit"のようなパイプ区切りで複数ツールを指定でき、省略すると対象イベントの全発火で呼ばれます。MCPツールはmcp__<server>__<action>という命名なので、matcher: "^mcp__"のような正規表現で横断的に拾えます。

複数のコールバックを同じイベントに登録すると、すべて並列に実行されます。許可判断では最も制限が強い結果が勝ち、1つでもdenyを返せば呼び出しはブロックされます。完了順は保証されないため、他のコールバックの実行結果に依存しないよう独立に書くのが安全です。

入力と出力 — 許可・拒否・書き換えの返し方

出力オブジェクトは2種類のフィールドを持ちます。systemMessagecontinueのようなトップレベルのフィールドはどのイベントでも共通です。hookSpecificOutputは現在の操作を細かく制御するための入れ物で、イベントごとに使えるフィールドが変わります。

PreToolUseではpermissionDecision"allow""deny""ask""defer"のいずれかを設定します。updatedInputを添えると、ツールに渡す引数そのものを書き換えられます。PostToolUseではadditionalContextでツール結果に情報を追記でき、updatedToolOutputでツールの出力自体を差し替えることもできます。

副作用だけが目的で結果を待たせたくないときは、非同期出力を使います。async: true(Pythonはasync_)を返すと、エージェントはコールバックの完了を待たずに処理を続けます。ログ送信やメトリクス収集のような、判断に関わらない処理に向いた形です。ただしブロックや入力変更はできません。エージェントがすでに次へ進んでいるためです。

実装例 — 複数のHooksを組み合わせる

同じイベントに複数のコールバックを登録すると、それぞれが独立に判定を返します。次の例はPreToolUseに3つの独立したチェックを並べています。

options = ClaudeAgentOptions(
    hooks={
        "PreToolUse": [
            HookMatcher(hooks=[authorization_check]),
            HookMatcher(hooks=[input_validator]),
            HookMatcher(hooks=[audit_logger]),
        ]
    }
)

matcherを使い分ければ、1つのイベントに複数の役割を持たせられます。パイプ区切りの完全一致(Write|Edit|NotebookEdit)でファイル編集系ツールだけに絞ったコールバック、正規表現(^mcp__)でMCPツール全体に絞ったコールバック、matcher省略で全ツールに効くログ用コールバックを、それぞれ別のコールバック関数として並べて登録できます。

/etcディレクトリへの書き込みを拒否する例では、systemMessageでユーザーへの通知、hookSpecificOutputでブロック理由の伝達を分けて返します。

const blockEtcWrites: HookCallback = async (input, toolUseID, { signal }) => {
  const preInput = input as PreToolUseHookInput;
  const toolInput = preInput.tool_input as Record<string, unknown>;
  const filePath = toolInput?.file_path as string;
 
  if (filePath?.startsWith("/etc")) {
    return {
      systemMessage: "Remember: system directories like /etc are protected.",
      hookSpecificOutput: {
        hookEventName: preInput.hook_event_name,
        permissionDecision: "deny",
        permissionDecisionReason: "Writing to /etc is not allowed"
      }
    };
  }
  return {};
};

permissionDecisionReasonはモデルに、systemMessageはユーザーに、それぞれ別の宛先へ届きます。モデルに再試行を諦めさせたい理由と、ユーザーに知らせたい注意事項が違う場合に使い分けます。

実装例 — HTTPリクエストとSlack通知を送る

PostToolUseでWebhookに通知を送る例です。エラーはコールバック内で必ず捕捉します。捕捉し損ねた例外はエージェントの処理を中断させるおそれがあるためです。

async def webhook_notifier(input_data, tool_use_id, context):
    if input_data["hook_event_name"] != "PostToolUse":
        return {}
    try:
        await asyncio.to_thread(_send_webhook, input_data["tool_name"])
    except Exception as e:
        print(f"Webhook request failed: {e}")
    return {}

Notificationイベントを使えば、権限プロンプトや入力待ちの通知をSlackへ転送できます。SDKセッションではpermission_prompt(canUseToolコールバックが約6秒待った場合)、elicitation_completeelicitation_responseの3種類がこの経路で発火します。idle_promptのような対話UI起点の通知は、SDKセッションが持たないインターフェースからの発火なので届きません。

つまずきやすい点

コールバック内で例外を投げると、try/except(TypeScriptはtry/catch)で捕まえない限りエージェントの実行に影響します。特にHTTPリクエストやファイルI/Oを含むコールバックでは、失敗時にログだけ残して{}を返す設計が安全です。

matcherはツール名にしかマッチしません。ファイルパスなど引数の中身で絞りたい場合は、コールバック内でtool_inputを読んで判定する必要があります。matcherだけで細かい条件分岐を組もうとすると意図した通りに動きません。

サブエージェントを複数起動すると、それぞれが個別に権限を要求することがあります。PreToolUseで特定ツールを自動承認するか、親セッションから継承される権限ルールを設定しておくと、確認プロンプトの多発を避けられます。

タイムアウトはイベントごとに既定値が違う

コールバックには実行時間の上限があります。HookMatchertimeoutフィールドで秒単位に指定でき、省略時はイベントごとの既定値が適用されます。多くのイベントは600秒、UserPromptSubmitは30秒です。上限を超えるとClaude Codeはコールバックをキャンセルし、失敗したHookとして扱います。出力は破棄され、セッションは停止せずに続行されます。

PreToolUseのコールバックがタイムアウトすると、Claude Codeはそのツール呼び出しを実行しません。Claudeにはフックが応答しなかった旨のツール結果が渡され、ターンは継続します(他のPreToolUseフックが明示的にdenyを返していれば、そちらのdeny理由が優先されます)。長い処理を挟むコールバックでは、timeoutを明示的に長く設定するか、非同期出力に切り替える判断が必要です。

SDKのHooksとCLIのHooksは併用できるか

SDKアプリケーションがsetting_sources(settingSources)でプロジェクト設定を読み込めば、.claude/settings.jsonのシェルコマンド型Hooksとコールバック型Hooksは同じセッション内で共存します。両方が同じイベントにマッチする場合、判断の優先順位はdeny > defer > ask > allowの順です。

hookSpecificOutputhookEventNameを含め忘れると、updatedInputが反映されません。ネストの位置を間違え、トップレベルに置いてしまう間違いもよくあります。hookSpecificOutputの内側に置くことを確認してください。

よくある質問

matcherは正規表現で書けますか

書けます。ツール名を英数字・アンダースコア・ハイフン・空白・,|だけで構成すると完全一致(または|/,区切りの完全一致リスト)として評価され、それ以外の文字を含むと正規表現(先頭・末尾を固定しない部分一致)として評価されます。この評価規則はCLIのsettings.jsonにおけるmatcherパターンと共通です。

コールバックの中でツールを直接呼び出せますか

PreToolUsePostToolUseのコールバック自体はツール実行の制御に特化しており、Claude本体のようにBashやReadを呼び出す機構は持ちません。外部処理が必要な場合は、コールバックの中で自前の関数やHTTPリクエストを直接実行します。

TypeScript SDKとPython SDKのどちらを選べばよいですか

対応イベント数が多いのはTypeScript SDKです。PostToolBatchMessageDisplayInstructionsLoadedのような比較的新しいイベントを使いたい場合はTypeScript一択になります。ツール系・セッション系の主要イベントだけで足りるならPython SDKでも設計上の制約はほぼありません。

まとめ

Agent SDKのHooksは、Claude Code CLIのsettings.jsonとは別の、コールバック関数として書く仕組みです。options.hooksにイベント名をキーとして登録し、matcherで対象を絞り、返り値のhookSpecificOutputで許可・拒否・入力の書き換えを指示します。TypeScript SDKのほうが対応イベントが多く、SessionStart/SessionEndのようにPython SDKでは使えないイベントもあります。CLIのHooksと同じ語彙で設計されているぶん理解は流用できますが、実装コードはCLI向けのシェルスクリプトをそのまま持ち込めない点に注意が必要です。

CLIのHooksを一通り把握したい場合はClaude Code Hooks完全ガイド、コピペで使えるレシピ集はClaude Code Hooks実例カタログを参照してください。Agent SDKそのものの始め方はAgent SDKクイックスタートにまとめています。

この記事を共有:XはてブLinkedIn