Claude Agent SDK のフックは、ツールが呼ばれる直前・直後、サブエージェントの開始や終了、実行の停止といったタイミングで、あなたのコード(コールバック関数)を実行するしくみです。危険な操作のブロック、全ツール呼び出しの監査ログ、入力の書き換え、外部サービスへの通知などをアプリ側のコードで実装できます。
この記事では、公式ドキュメント「Intercept and control agent behavior with hooks」に沿って、options.hooks と matcher の書き方、コールバックが受け取る引数と返す値、Python と TypeScript で使えるイベントの違い、タイムアウト時の挙動までを整理します。CLI の settings.json に書くシェルコマンド型のフックは「Claude Code のフックで操作を自動化する」で扱っています。
フックの仕組みと最初の例
公式ドキュメントは、フックが動く流れを次の5段階で説明しています。
- イベントが発生する … ツール呼び出しの直前(
PreToolUse)、ツールの結果が返った直後(PostToolUse)、サブエージェントの開始・終了、実行の終了など。 - SDK が登録済みのフックを集める …
options.hooksに渡したコールバックに加え、settingSources(Python ではsetting_sources)で読み込む設定ファイルに書かれたシェルコマンド型のフックも対象です。query()の既定のオプションでは設定ファイルも読み込まれます。 - matcher で絞り込む …
"Write|Edit"のようなパターンがあれば、イベントの対象(ツール系ならツール名)と照合します。matcher の無いフックは、そのイベントで毎回実行されます。 - コールバックが実行される … ツール名・引数・セッション ID などのイベント情報を受け取ります。
- コールバックが判断を返す … 許可・ブロック・入力の書き換え・会話への文脈追加などを、返り値のオブジェクトで指示します。
次の例は、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 ではオブジェクトです。
- キー …
'PreToolUse'・'PostToolUse'・'Stop'などのイベント名(大文字小文字を区別します)。 - 値 … matcher の配列。各要素が「絞り込みパターン」と「コールバック関数の配列」を持ちます。
matcher の各項目は次のとおりです。
| 項目 | 型 | 内容 |
|---|---|---|
matcher | string(省略可) | イベントの対象と照合するパターン。ツール系のイベントではツール名(Bash、Read、Write、Edit、Glob、Grep、WebFetch、Agent など)。MCP ツールは mcp__<server>__<action> の形です。 |
hooks | HookCallback[](必須) | パターンに一致したときに実行するコールバックの配列。 |
timeout | number(省略可) | 秒単位のタイムアウト。省略時はイベントごとの既定値が使われます。 |
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つの引数
- 入力データ … イベントごとに型の決まったオブジェクトです。たとえば
PreToolUseHookInputにはtool_nameとtool_inputが入ります。すべてのイベントに共通してsession_id・cwd・hook_event_nameがあり、サブエージェント内で発生した場合はagent_idとagent_typeも入ります。 - ツール使用 ID … 同じツール呼び出しの
PreToolUseとPostToolUseを対応付けるための ID です。 - コンテキスト … TypeScript ではキャンセル用の
signal(AbortSignal)を持ちます。Python では将来のための予約引数です。
返り値の2種類のフィールド
- トップレベル …
systemMessage(ユーザーに見せるメッセージ)とcontinue(Python ではcontinue_。フックの後もエージェントを続けるか)。 hookSpecificOutput… 現在の操作を制御します。PreToolUseではpermissionDecision("allow"・"deny"・"ask"・"defer")、permissionDecisionReason、updatedInputを指定します。PostToolUseではadditionalContextでツール結果に情報を追記したり、updatedToolOutputで Claude に渡る出力を差し替えたりできます。
複数のフックや権限ルールが同時に当てはまる場合は、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 リクエストを送るフックでは、例外を外へ投げずにフックの中で捕まえるよう公式は注意しています。
うまく動かないときの確認点
- フックが発火しない … イベント名の綴りと大文字小文字(
PreToolUseであってpreToolUseではない)、options.hooksの正しいイベントの下に置いているかを確認します。Stopは matcher を無視します。max_turnsの上限に達するとセッションが先に終わり、フックが動かないことがあります。 - matcher で絞り込めない … matcher が照合するのはツール名だけで、ファイルパスなどの引数は対象外です。パスで分けたいときは、コールバックの中で
tool_input.file_pathを調べます。 - 書き換えが反映されない …
updatedInputはトップレベルではなくhookSpecificOutputの中に置き、hookEventNameも入れます。 - systemMessage が見えない …
systemMessageはユーザー向けで、モデルには渡りません。モデルに文脈を渡したいときはadditionalContextを使います。 - サブエージェントで確認が増える・ループする … サブエージェントは自分のツール呼び出しごとに権限を求めることがあるため、
PreToolUseでの自動承認や権限ルールで抑えます。サブエージェントを起動するUserPromptSubmitフックは、同じフックが再び発火して無限ループになり得るので、トップレベルのセッションに限定します。
タイムアウトしたとき
timeout を指定しない場合の既定は、多くのイベントで600秒、UserPromptSubmit・PreModelSwitch・PostModelSwitch で30秒、MessageDisplay で10秒です。タイムアウトしたコールバックは取り消され、その出力は捨てられますが、セッションは止まらずに続きます。その後の扱いはイベントで異なります。
PreToolUse… ツールは実行されず、Claude には「フックが時間内に応答しなかった」というツール結果が返ります。PostToolUse/PostToolUseFailure… ツールの結果はそのまま残ります。UserPromptSubmit… プロンプトはブロックされます(ポリシーのゲートとして使われ得るため、確認されないまま通すことはしない)。Stop/SubagentStop… そのコールバックは「判断なし」とみなされ、他のフックの判断は有効なままです。
時間のかかる処理では 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)。イベント名・フィールド名は公式表記のまま記載しています。最新の仕様は公式ドキュメントを確認してください。