Agent SDK のフック:PreToolUse でツール実行を止める・書き換える

Claude Agent SDK エンジニアリング 8分で読めます

Claude Agent SDK のフックは、ツールが呼ばれる直前・直後、サブエージェントの開始や終了、実行の停止といったタイミングで、あなたのコード(コールバック関数)を実行するしくみです。危険な操作のブロック、全ツール呼び出しの監査ログ、入力の書き換え、外部サービスへの通知などをアプリ側のコードで実装できます。

この記事では、公式ドキュメント「Intercept and control agent behavior with hooks」に沿って、options.hooks と matcher の書き方、コールバックが受け取る引数と返す値、Python と TypeScript で使えるイベントの違い、タイムアウト時の挙動までを整理します。CLI の settings.json に書くシェルコマンド型のフックは「Claude Code のフックで操作を自動化する」で扱っています。

フックの仕組みと最初の例

公式ドキュメントは、フックが動く流れを次の5段階で説明しています。

  1. イベントが発生する … ツール呼び出しの直前(PreToolUse)、ツールの結果が返った直後(PostToolUse)、サブエージェントの開始・終了、実行の終了など。
  2. SDK が登録済みのフックを集める … options.hooks に渡したコールバックに加え、settingSources(Python では setting_sources)で読み込む設定ファイルに書かれたシェルコマンド型のフックも対象です。query() の既定のオプションでは設定ファイルも読み込まれます。
  3. matcher で絞り込む … "Write|Edit" のようなパターンがあれば、イベントの対象(ツール系ならツール名)と照合します。matcher の無いフックは、そのイベントで毎回実行されます。
  4. コールバックが実行される … ツール名・引数・セッション ID などのイベント情報を受け取ります。
  5. コールバックが判断を返す … 許可・ブロック・入力の書き換え・会話への文脈追加などを、返り値のオブジェクトで指示します。

次の例は、Write / Edit ツールが .env ファイルを対象にしたときだけ permissionDecision: "deny" を返して書き込みを止めるものです(公式の例をそのまま掲載しています)。

import { query, HookCallback, PreToolUseHookInput } from "@anthropic-ai/claude-agent-sdk";

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 {};
};

for await (const message of query({
  prompt: "Create a .env file with the standard local development database configuration",
  options: {
    hooks: {
      PreToolUse: [{ matcher: "Write|Edit", hooks: [protectEnvFiles] }]
    }
  }
})) {
  if (message.type === "assistant" || message.type === "result") {
    console.log(message);
  }
}

Python では HookMatcher を使って同じことを書きます。コールバックは async def で、引数は (input_data, tool_use_id, context) の3つです。

from claude_agent_sdk import ClaudeAgentOptions, HookMatcher

async def protect_env_files(input_data, tool_use_id, context):
    file_path = input_data["tool_input"].get("file_path", "")
    if file_path.split("/")[-1] == ".env":
        return {
            "hookSpecificOutput": {
                "hookEventName": input_data["hook_event_name"],
                "permissionDecision": "deny",
                "permissionDecisionReason": "Cannot modify .env files",
            }
        }
    return {}

options = ClaudeAgentOptions(
    hooks={"PreToolUse": [HookMatcher(matcher="Write|Edit", hooks=[protect_env_files])]}
)

どちらも、何もしないで通す場合は空のオブジェクト {} を返します。

使えるフックイベント

イベントには両方の SDK で使えるものと、TypeScript SDK だけのものがあります。Python SDK の HookEvent 型に含まれるのは次の10種類です。

イベント発生するタイミング用途の例
PreToolUseツール呼び出しの要求時(ブロック・書き換え可)危険なシェルコマンドを止める
PostToolUseツールの実行結果が返ったときファイル変更を監査ログに残す
PostToolUseFailureツールの実行が失敗したときツールのエラーを記録する
UserPromptSubmitプロンプトが送信されたときプロンプトに文脈を追加する
Stopエージェントの実行が止まるとき終了前にセッション状態を保存する
SubagentStart / SubagentStopサブエージェントの開始・終了並列タスクの追跡・結果の集約
PreCompact会話の圧縮(compaction)の要求時要約前に全文を保存する
PermissionRequestツール呼び出しに権限の判断が必要なとき独自の承認処理
Notificationエージェントの状態通知Slack などへ転送する

TypeScript SDK ではさらに、SessionStart / SessionEnd、PostToolBatch、PostCompact、PreModelSwitch / PostModelSwitch、PermissionDenied、StopFailure、TaskCreated / TaskCompleted、ConfigChange、InstructionsLoaded、WorktreeCreate / WorktreeRemove、CwdChanged、FileChanged なども使えます。イベントの一覧は SDK のバージョンで増えていくため、使う前に公式の表で対応状況を確認してください。

Python で SessionStart / SessionEnd を使いたい場合は、SDK のコールバックではなく .claude/settings.json などにシェルコマンド型フックとして書き、setting_sources=["project"] のようにその設定ファイルを読み込ませます。

options.hooks と matcher の書き方

hooks オプションは、Python では辞書、TypeScript ではオブジェクトです。

matcher の各項目は次のとおりです。

項目型内容
matcherstring(省略可)イベントの対象と照合するパターン。ツール系のイベントではツール名(Bash、Read、Write、Edit、Glob、Grep、WebFetch、Agent など)。MCP ツールは mcp__<server>__<action> の形です。
hooksHookCallback[](必須)パターンに一致したときに実行するコールバックの配列。
timeoutnumber(省略可)秒単位のタイムアウト。省略時はイベントごとの既定値が使われます。

matcher の書き方は、settings.json のフックと同じ規則に従います。公式の例では、パイプ区切りの完全一致リスト(Write|Edit|NotebookEdit)、正規表現(^mcp__ で MCP ツール全体)、matcher の省略(全ツール)を使い分けています。

options = ClaudeAgentOptions(
    hooks={
        "PreToolUse": [
            HookMatcher(matcher="Write|Edit|NotebookEdit", hooks=[file_security_hook]),
            HookMatcher(matcher="^mcp__", hooks=[mcp_audit_hook]),
            HookMatcher(hooks=[global_logger]),
        ]
    }
)

公式は、できるだけ matcher で対象ツールを絞るよう勧めています。'Bash' を指定すれば Bash コマンドのときだけ動き、省略するとそのイベントのたびに毎回動きます。全ツール呼び出しを記録したいときだけ、意図して省略します。

コールバックの入力と返り値

受け取る3つの引数

返り値の2種類のフィールド

複数のフックや権限ルールが同時に当てはまる場合は、deny > defer > ask > allow の順で優先されます。1つでも deny を返せば、他のフックの結果にかかわらずブロックされます。"defer" を返すと、そのターンは stop_reason が "tool_deferred" の結果メッセージで終わり、あとで呼び出しを再開できます。

非同期出力(待たせない)

ログ送信のように、エージェントの動きに影響しない副作用だけを行うフックは、async: true(Python では async_: True)を返すと、エージェントはフックの完了を待たずに先へ進みます。asyncTimeout でバックグラウンド処理のタイムアウトをミリ秒で指定できます。この場合、操作のブロック・書き換え・文脈追加はできません。

const asyncHook: HookCallback = async (input, toolUseID, { signal }) => {
  sendToLoggingService(input).catch(console.error);
  return { async: true, asyncTimeout: 30000 };
};

実用パターン

ツールの入力を書き換える

PreToolUse で updatedInput を返すと、ツールに渡る引数を変更できます。公式の例は、Write ツールの file_path の先頭に /sandbox を付け、書き込み先をサンドボックス用ディレクトリへ振り向けるものです。

async def redirect_to_sandbox(input_data, tool_use_id, context):
    if input_data["hook_event_name"] != "PreToolUse":
        return {}
    if input_data["tool_name"] == "Write":
        original_path = input_data["tool_input"].get("file_path", "")
        return {
            "hookSpecificOutput": {
                "hookEventName": input_data["hook_event_name"],
                "permissionDecision": "allow",
                "updatedInput": {
                    **input_data["tool_input"],
                    "file_path": f"/sandbox{original_path}",
                },
            }
        }
    return {}

updatedInput と permissionDecision: 'allow' を組み合わせると書き換えた入力が自動承認され、'ask' ならユーザーに確認が回ります。permissionDecision を省略しても書き換えは適用され、通常の権限評価に進みます。'defer' と組み合わせると updatedInput は無視されます。元の tool_input を直接変更せず、新しいオブジェクトを返すのが原則です。

ブロックして理由を伝える

permissionDecision: 'deny' で呼び出しを止め、permissionDecisionReason でモデルに理由を伝え(同じ操作の再試行を避けさせる)、systemMessage でユーザーにも状況を見せる、という3つを1つの返り値にまとめられます。公式の例では /etc 配下への書き込みをこの形で拒否しています。

読み取り専用ツールを自動承認する

tool_name が Read・Glob・Grep のときだけ permissionDecision: 'allow' を返せば、それらは確認なしで実行され、他のツールは通常の権限チェックのまま残ります(ネットワークパスからの読み取りは除きます)。

複数のフックを並べる

1つのイベントに一致したフックは並列に実行されます。完了順は決まっていないため、「別のフックが先に動いている」ことを前提にせず、それぞれ独立して動くように書きます。権限の判断は最も厳しい結果が採用されます。

サブエージェントの終了と通知の転送

SubagentStop では agent_id や agent_transcript_path を受け取れるので、サブエージェントごとの完了ログを残せます。Notification フックを使えば、権限リクエストが canUseTool コールバックで約6秒待たされたときの permission_prompt などの通知を、Slack の Incoming Webhook へ転送できます。HTTP リクエストを送るフックでは、例外を外へ投げずにフックの中で捕まえるよう公式は注意しています。

うまく動かないときの確認点

タイムアウトしたとき

timeout を指定しない場合の既定は、多くのイベントで600秒、UserPromptSubmit・PreModelSwitch・PostModelSwitch で30秒、MessageDisplay で10秒です。タイムアウトしたコールバックは取り消され、その出力は捨てられますが、セッションは止まらずに続きます。その後の扱いはイベントで異なります。

時間のかかる処理では timeout を大きくし、TypeScript では3番目の引数の signal を fetch などに渡して、タイムアウト時にきれいに中断できるようにします。

権限まわりの全体像(フック・deny ルール・権限モード・canUseTool の評価順)は「Agent SDK の権限設定」で解説しています。

本ページは Intercept and control agent behavior with hooks の内容に基づく非公式の日本語解説で、Python で使えるイベントは Agent SDK reference - Python の HookEvent 定義と照合しています(確認日 2026-10-12)。イベント名・フィールド名は公式表記のまま記載しています。最新の仕様は公式ドキュメントを確認してください。