Claude Code Hooksの5タイプと、コマンド型が重くなる理由

Hooksは5タイプを設定で選択、同じイベントでは並列に動作

Hooksの5タイプ(settings.jsonに書くtypeの値)

command
シェルコマンドを起動し、イベントのJSONを標準入力で受け取る
http
イベントのJSONを指定したURLへPOSTで送信する
mcp_tool
設定済みのMCPサーバーにあるツールを呼ぶ
prompt
Claudeのモデルに1回の応答で判定させる
agent
ツールを使うことができるサブエージェントに調べさせる(実験的)

Claude Code Hooksは、セッション開始やツール実行の前後といったタイミングで、登録しておいた処理を自動で実行する仕組みです。 実行する処理はハンドラーと呼ばれ、settings.jsonに書くtypeの値で5タイプから選ぶことができます。仕様は、Anthropic公式のHooks referenceに掲載されています。

Claude Code Docsの「Hooks reference」ページの冒頭。右側の目次にHook handler fieldsとCommand・HTTP・MCP toolの各名称が掲載されている
5タイプそれぞれの設定内容は、このページのHook handler fieldsの部分にあります。Claude Code Docs — Hooks reference

command・http・mcp_toolなど5タイプの動きと設定

commandはシェルコマンドを起動するタイプで、イベントの内容をJSONで標準入力から受け取り、終了コードと標準出力で結果を伝えます。 ローカルのスクリプトをそのまま呼べるので、Hooksを使うときに最も選びやすいタイプです。

httpは、イベントのJSONを指定したURLへPOSTで送信します。応答の本文はcommandと同じJSONの形式で返せばよいので、判定の処理を常駐しているサーバー側に置けます。mcp_toolは設定済みのMCPサーバーにあるツールを呼び出し、ツールが出力したテキストの処理はcommandの標準出力と同じです。

promptとagentは、判定をモデルに任せるタイプです。promptはClaudeのモデルへプロンプトを送信し、1回の応答でJSONの判定を返させます。

agentはRead・Grep・Globなどのツールを使うことができるサブエージェントを起動し、条件を調べてから判定を出力する仕組みです。Anthropicはagentを実験的な機能としており、今後変わる可能性があります。また、すべてのイベントが5タイプすべてに対応しているわけではありません。

"send the event’s JSON input as an HTTP POST request to a URL. The endpoint communicates results back through the response body using the same JSON output format as command hooks."
公式情報を見る →

同じイベントのHookは並列で動作し、タイムアウトの設定はタイプ別

同じイベントに登録したHookが複数あると、Claude Codeはそれらを順番ではなく並列に実行します。 Hookを登録した数だけ、イベントのたびにプロセスやリクエストが同時に起こるということです。イベントは発生する頻度で次の3つに分類でき、下にあるものほど回数が多くなります。

  • セッションごと:SessionStart・SessionEnd
  • ターンごと:UserPromptSubmit・Stopなど
  • ツール呼び出しごと:PreToolUse・PostToolUse

タイムアウトのデフォルト値はタイプによって異なり、UserPromptSubmitなどのイベントではcommand・http・mcp_toolも30秒に短縮されます。他のイベントでのデフォルト値は次のとおりです。

  • command・http・mcp_tool:600秒
  • prompt:30秒
  • agent:60秒
"Defaults: 600 for command , http , and mcp_tool ; 30 for prompt ; 60 for agent ."
公式情報を見る →

commandだけのHooksはnode起動でCPU・メモリの負荷が重い

ツールを1回呼んだときに起動するnodeの本数

EditとWriteは1回で17本と最も多く、Grepでも6本が起動する。ツールを呼ぶ回数が多いほど、この本数がそのまま積み重なる。棒の高さは最大値を100とした比。

17Edit
17Write
12Bash
10Agent
8Read
6Grep

書き手の環境ではHooksをすべてcommandタイプで作っていたため、ツールを1回呼ぶたびにnodeが何本も起動していました。 当初はHooksに5タイプあることを知らず、どのHookもnodeで動作するスクリプトとして書いていました。計測に使ったのはM4のMac(メモリ16GB)で、Claude Code 2.1.289のセッションを10〜13本並べて動作させた状態です。

Edit 1回でnode 17本、Stopではgitも約209本が起動

本番と同じ設定のHookを手で起動してカウントしたところ、Editを1回呼ぶだけでnodeが17本起動していました。 Writeも同じ17本です。ほかはBash(短いコマンド)が12本でAgentが10本、Readが8本でGrepが6本でした。

ツール呼び出しごとのHookは、Claudeがツールを使うたびに発火します。1ターンの中でEditを何度も呼べば、そのたびに17本ずつ起動が重なります。ターンの終わり(Stop)では、nodeに加えてgitが約209本起動していました。

node 1回の起動はCPU約29ms・メモリ44MB

プロセス1回の起動にかかるCPU時間(ms)

nodeが使うCPU時間は/bin/shの約9倍に達する。数行にとどまる判定でも、nodeで書けば起動のたびにこの差を払う。棒の高さは最大値を100とした比。

29node
4.7/usr/bin/curl
3.3/bin/sh

nodeを1回起動するコストを計測すると、CPU時間が約29ms、メモリ(RSS)が44MBでした。ここでいう起動のコストとは、処理の中身を実行する前にプログラムを読み込んで動作する状態にするまでの負担です。

比較のために計測した/bin/shは1回3.3msで2MB、/usr/bin/curlは4.7msでした。一方、nodeはCPU時間が/bin/shの約9倍でメモリは22倍です。 Hookの中身が数行の判定でも、nodeで書けば起動のたびにこの分がかかります。

複数セッションで遅いMacは、node起動の47〜87%がHook由来

マシン全体でnodeの起動回数をpidの監視でカウントすると毎秒9.6〜15.9回で、そのうち47〜87%がHooksから起動したものでした。 セッションを10〜13本並べると、ツール呼び出しで起動するnodeが重なり、マシン全体で起動するnodeの半分から9割近くをHooksが占めていました。Macが遅いときは、まずHooksが起動するnodeの本数を調べることで負荷の出どころを確認できます。

Claude Codeの重さは、モデルへ送信するトークンの量からも生じます。起動時に送信されるトークンも、Hooksの本数とは別に負荷を生む要因です。

Stop等のHook集約でプロンプト送信ごとのnodeを42本から1本に

ターン境界のHookを集約する前と後の起動本数

プロンプト送信ごと

42本が1本になり、3イベント中で減少率が最も大きい。

42集約前
1集約後

セッション開始ごと

起動本数が最も多かった48本も、3本まで減少した。

48集約前
3集約後

ターン終わりごと

40本が約3本になり、3つのイベントすべてで起動が1桁に収まった。

40集約前
約3集約後

ターン境界で動作するHookを1つのプロセスにまとめたところ、プロンプト送信ごとに起動するnodeは42本から1本に減少しました。 まとめたのは、プロンプト送信・セッション開始・ターン終わりという3イベントで動作するHookです。それまで別々に起動していたスクリプトの判定処理を、1つのプロセス内から呼び出すよう改修しています。

SessionStartは48本から3本、Stopは40本から約3本へ

セッション開始(SessionStart)では起動が48本から3本に、ターン終わり(Stop)では40本から約3本になりました。集約前後の本数は、いずれもイベント1回あたりの数です。

また、まとめても判定が変わらないことは、過去12ターンの入力を集約後の構成で再生して確認しました。 拒否するか通すかの判定と差し込む文を照合し、違いは0でした。

重い時間帯に通知音が途切れた原因は何か

集約を進める途中で、ターンの終わりに鳴らす通知音がマシンの重い時間帯に途切れるようになりました。 原因は通知音を再生するプロセスを終了を待たずに切り離すよう変更したことでした。切り離した子プロセスを、Hookを管理する側の仕組みが低い優先度(background QoS)へ落としていたのです。QoSはmacOSがプロセスを実行する優先度を定める仕組みで、backgroundは優先度の低い種類です。

この仕組みには例外がありましたが、例外に該当するかどうかは起動元のファイル名だけで判定していました。通知音を鳴らすafplayは例外に含まれず、低い優先度のまま再生されていたわけです。afplayも例外に加えると、スケジューラの優先度は4から31に上昇し、音は途切れなくなりました。

一度出した複数セッションのnode起動回数は誤り

「複数セッションのnode起動が9分で5万3,492回」という数字を一度出しましたが、誤りでした。カウントしていたのはカーネルログの行で、その1行はnodeの起動1回に対応していませんでした。

そこで、pid(プロセスに振られる番号)の監視でカウントし直すと、マシン全体のnode起動は毎秒9.6〜15.9回でした。 Hooks由来の割合47〜87%も、このカウントし直した値をもとに出しています。極端に大きな数字が出たときは、結論を出す前に何を1回としてカウントしているのかを疑うべきでした。

Hooksを設定するならメモリとCPUの負荷が軽いタイプから選ぶ

Hookのタイプを選ぶ順(負荷の軽い順)

①既存のバッチ
集約したプロセスの中で動作させる。プロセス数は増加しない
②http
常駐するサーバーへPOSTする。プロセスの起動は無い
③mcp_tool
MCPサーバーのツールを呼ぶ
④小さなコマンド
Apple標準の小さなコマンドを起動する
⑤node
nodeを起動する
⑥prompt・agent
モデルに判定させる。拒否するか通すかの判定には使わない

Hookを作るときや修正するときは、役割を果たせるもののうち、マシンの負荷が最も軽いタイプを選ぶことにしました。 ここでいう負荷は、Hookが動作するたびに使うメモリとCPUです。軽い順に並べると上の6段で、上の段で役割を果たせるなら下の段は使いません。

nodeを減らすなら既存のバッチ・http・mcp_toolの順に選ぶ

負荷が最も軽いのは既存の集約プロセス内で動作させる場合で、新たな起動は0本です。 次がhttpで、常駐しているサーバーへPOSTするだけなので、Hookのたびにプロセスを起動しません。その次がmcp_toolで、Apple標準の小さなコマンド、nodeの起動と続きます。

httpを利用する際は、応答コードの解釈を事前に確認しておくといいでしょう。2xx以外の応答や接続の失敗はブロックしないエラーとして処理されるため、ツールの実行を拒否したいときは2xxで応答し、その本文に判定のJSONを入れる必要があります。なお、書き手の環境ではhttpタイプへの移行は設計中で、受け手になる常駐サーバーはまだありません。

"To block a tool call or deny a permission, return a 2xx response with a JSON body containing the appropriate decision fields ."
公式情報を見る →

promptとagentはPreToolUseで不採用

promptとagentはモデルが判定するため、同じ入力でも結果が毎回同じになるとは限りません。 そこで、PreToolUseでツールの実行を拒否するか通すかの判定には使わないことにしました。

呼ぶたびにAPIの呼び出しかサブエージェントの起動が発生するので、負荷の面でも最も重い段に置いています。Anthropicもagentタイプについて、本番の運用ではcommandタイプを使うよう勧めています。

"For production workflows, prefer command hooks ."
公式情報を見る →

タイプの宣言行が無いHookはsettings.jsonへ設定させない

Hookファイルの先頭には、採ったタイプとより軽いタイプを採らない理由を書いた宣言行を、必ず置くことにしました。 宣言行が無いファイルの作成・編集・settings.jsonへの登録は、機械で拒否されます。選ぶ順を文書に書いて終わりにせず、宣言の無いHookはそもそも作ることができない仕組みにしています。

全部commandで作らず、負荷の軽いタイプから選ぶ

Claude Code Hooksのハンドラーは5タイプあり、すべてcommandで作ると、ツール呼び出しのたびにnodeの起動が積み重なります。 書き手の環境ではEdit 1回で17本が起動し、マシン全体でnodeを起動した回数の47〜87%をHooksが占めていました。

まずはご自身の環境で、ツール呼び出し1回あたりに起動するプロセスの本数をカウントしてみてください。そのうえでターン境界のHookを1つのプロセスへまとめると、書き手の環境ではプロンプト送信ごとの起動が42本から1本になりました。新しく作るHookは、既存のバッチ・http・mcp_toolといった負荷が軽いタイプから検討することをおすすめします。

FAQ

Claude Code Hooksのタイプは何種類ですか?
Hooksのタイプは、command・http・mcp_tool・prompt・agentの5種類です。settings.jsonのtypeで指定し、たとえばhttpはイベントのJSONをURLへPOSTし、mcp_toolはMCPサーバーのツールを呼び出します。
同じイベントに登録したHookは順番に実行されますか?
順番ではなく並列に実行されます。同じイベントに登録したHookが複数あれば、その数だけプロセスの起動やリクエストの送信が同時に発生します。
promptやagentのタイプで危険なコマンドを拒否してもよいですか?
拒否するか通すかを毎回同じ結果にしたい判定には適していません。promptとagentはモデルが判定するため、同じ入力でも結果が同じになるとは限りません。agentについては、Anthropicも本番の運用ではcommandタイプを勧めています。