PostToolBatch(ポストツールバッチ)

フック
PostToolBatch
ポストツールバッチ
Claudeが同時に走らせたツールの一束(batch)が全部終わった後、次にClaude本体へ問い合わせる前に、ちょうど1回だけ動くフック。ツール1個ごとに動くPostToolUseと違い、走ったツールの組み合わせを見てまとめて1回だけ割り込める

PostToolUseフックを使っていて、Claudeが並列でツールを動かしたときに全部終わった後で1回だけ何かしたい人向け

Claudeがファイルを並列で複数読むなど、ツールをまとめて同時に動かしたあと、その組み合わせに応じた注意書き(例: このledger部分を読んだら作業完了前にpytestを回せ)を、束につき1回だけClaudeに差し込みたい場面で使う。ツール1個ごとに反応したいならPostToolUseを使う

Claude Codeはファイルを読むとき、1個ずつ順番に読むとは限りません。「accounts.pyとtransactions.pyを読んで」と頼むと、2本まとめて同時に走らせることがあります。この「同時に走った一束」が全部終わったあと、Claudeが次に考え始める直前に、ちょうど1回だけ割り込めるのがPostToolBatchフックです。

似た名前のPostToolUseは「ツール1個ごと」に動きます。だから同時に2本走れば2回呼ばれる。PostToolBatchは束に対して1回。「どのツールがセットで走ったか」を見て、まとめて1回だけ注意書きを差し込みたいときの置き場所です。

噛み砕くと

料理人が冷蔵庫から材料を一気に3つ取り出す場面を想像してください。PostToolUseは「卵を取った」「牛乳を取った」「バターを取った」と、材料1個ごとに横から声をかける係です。3個取れば3回しゃべる。

PostToolBatchは違います。3つ全部取り終わって、料理人がさあ次どうするかと考え始める手前で、1回だけ「その3つ、ケーキ作るやつですよね。先にオーブン予熱しておいて」と声をかける係です。個々の材料ではなく、取った材料の組み合わせを見て口を出す。だから「Readが2本含まれてる束のときだけ反応する」みたいな書き方ができます。

個別に反応したいか、束で1回反応したいか。そこが2つのフックの分かれ目です。

大事な前提:これは「同時に走った一束」が終わった直後に動く

PostToolBatchは、Claudeが並列で動かしたツール群が全部解決した後、次のモデル呼び出しの前に走ります。公式ドキュメントの説明はこうです。

Runs once after every tool call in a batch has resolved, before Claude Code sends the next request to the model. PostToolUse fires once per tool, which means it fires concurrently when Claude makes parallel tool calls. PostToolBatch fires exactly once with the full batch, so it is the right place to inject context that depends on the set of tools that ran rather than on any single tool. There is no matcher for this event.

最後の1文が地味に重要です。このイベントには絞り込み条件(matcher)が無い。だから「Readのときだけ動かす」みたいな指定をフックの登録側に書いても効きません。動くのは「全部の束」に対してです。絞りたければ、フック側の処理プログラムの中で「今回の束にReadは何個あった?」を自分で数えて分岐させます。

「家計簿アプリ開発」を例に、実際の手順を見る

家計簿アプリを作っていて、お金の出し入れを扱うledgerという部分を触っているとします。「ここのファイル群を読んだ束のときは、作業を完了扱いにする前に必ずpytestを回せ」という注意書きを、Claudeに自動でリマインドさせてみます。

ステップ1: 設定ファイルにPostToolBatchを登録する

まず .claude/settings.json に、PostToolBatchが起きたら自分のプログラムを呼ぶよう書きます。ここでmatcher欄は書きません。書いても無視されるからです。登録できたかは /hooks と打てば一覧で確認できます。

ステップ2: 「Readが複数あったときだけ反応する」プログラムを用意する

フックのプログラムには、その束の中身が丸ごと渡ってきます。中身から「Readが何個あるか」を数えて、複数あったときだけ注意書きを返す、という形にします。注意書きの返し方は公式の例がこれです。

{
  "hookSpecificOutput": {
    "hookEventName": "PostToolBatch",
    "additionalContext": "These files are part of the ledger module. Run pytest before marking the task complete."
  }
}

この additionalContext に入れた文字列が、次にClaudeが考え始める前に1回だけ差し込まれます。毎ツールごとではなく、束につき1回。ここを勘違いすると「なんで2回入らないの?」と悩むので注意です。

ステップ3: Claudeに並列Readを起こさせる

Claudeに「accounts.pyとtransactions.pyを読んで、重複してる処理を直して」と頼みます。Claudeは2ファイルを別々に読むより同時に読んだほうが速いと判断して、Readを2本まとめて走らせることがあります。これが並列Readで、ここでひとつのbatchができます。

ステップ4: 束が解決した直後にフックが1回走る

Read 2本が両方読み終わると、Claudeが「で、どう直そうか」と考え始める手前で、PostToolBatchが1回だけ発火します。プログラムは束の中身を受け取って、Readが2個あることを確認し、さっきの注意書きを返します。

ステップ5: 渡ってくる中身を見てみる

フックに渡ってくるデータは公式例だとこういう形です。tool_calls という配列に、その束で走った全ツールが1個ずつ並びます。

{
  "session_id": "abc123",
  "transcript_path": "/Users/.../.claude/projects/.../00893aaf-19fa-41d2-8238-13269b9b3ca0.jsonl",
  "cwd": "/Users/...",
  "permission_mode": "default",
  "hook_event_name": "PostToolBatch",
  "tool_calls": [
    {
      "tool_name": "Read",
      "tool_input": {"file_path": "/.../ledger/accounts.py"},
      "tool_use_id": "toolu_01...",
      "tool_response": "     1\tfrom __future__ import annotations\n     2\t..."
    },
    {
      "tool_name": "Read",
      "tool_input": {"file_path": "/.../ledger/transactions.py"},
      "tool_use_id": "toolu_02...",
      "tool_response": "     1\tfrom __future__ import annotations\n     2\t..."
    }
  ]
}

各ツールには tool_name(ツール名)、tool_input(渡された入力)、tool_use_id(その呼び出しの識別子)、tool_response(結果)の4つが入っています。ここで tool_response の中身に癖があります。公式の注記です。

The tool_response shape differs from PostToolUse's. PostToolUse passes the tool's structured Output object, such as {filePath: "...", success: true} for Write; PostToolBatch passes the serialized tool_result content the model sees.

つまりReadなら、ファイルの生の中身ではなく、Claudeが実際に見ている「行番号つきテキスト」がそのまま入ります。さっきのJSON例で " 1\tfrom..." と先頭に行番号がついているのはそのためです。ここでよく初心者がつまずきます。PostToolUseと同じ形だと思ってプログラムを書くと、想定と違う形が来て読み取りに失敗する。同じ「ツールの後」でも、渡ってくる結果の形は別物だと覚えておくのが安全です。

つまり PostToolBatch は何をしてくれるのか

  • やってくれる: 同時に走った一束が全部終わった後、Claudeが次に考え始める前に、ちょうど1回だけ割り込んで注意書きを差し込む。「この組み合わせが走ったなら、次はこれをやれ」という束単位のリマインドに向く
  • やってくれない: ツール1個ごとの細かい反応。それはPostToolUseの仕事。matcherでツール名を絞ることもできない(書いても無視される)
  • 意味が薄い場面: そもそも並列で動かないツール、単発で1個だけ走るような操作。1個ずつ見たいだけならPostToolUseのほうが素直に書ける

使いどころ3シナリオ(具体題材で再現)

シナリオ1: 家計簿アプリのledger部分をまとめて読んだとき

Claudeがaccounts.pyとtransactions.pyを並列で読んだ束を見て、「この2つはお金の計算の中枢。直したら作業完了の前にpytestを必ず回せ」という注意書きを1回だけ差し込みます。個々のファイルではなく「この組み合わせが読まれた」ことに反応するので、PostToolBatchがぴったりです。1ファイルだけ読んだときは反応しない、という制御も束の中身を数えれば書けます。

シナリオ2: ブログ記事の複数Markdownを一括で書き換えたとき

料理ブログで、Claudeが記事のMarkdownを3本同時にWriteした束を捕まえます。3本まとめて書き換わったタイミングで「目次ページの更新も忘れずに」と1回だけリマインドを入れる。Writeごとに3回言われるとうるさいので、束で1回にまとまるこのフックが効きます。書き換え本数を数えて、2本以上のときだけ反応させると鬱陶しさが消えます。

シナリオ3: OSSをcloneした直後に設定ファイル群を読ませたとき

GitHubから持ってきたプロジェクトで、Claudeがpackage.jsonやtsconfigなどの設定ファイルを並列で読み込んだ束を見て、「このプロジェクトはpnpm運用。npm installは使うな」というプロジェクト固有のルールを1回だけ注入します。設定ファイルが読まれた瞬間という、Claudeが方針を立てる直前のタイミングを狙えるのがうれしいところです。

初心者が踏みやすい落とし穴

  • PostToolUseは並列でも1回だけだと思い込む。違います。ツール1個ごとに動くので、同時に2本走れば2回呼ばれます。束で1回にしたいならPostToolBatchです
  • matcherでReadだけに絞ろうとする。このイベントはmatcher非対応で、書いても黙って無視されます。エラーも出ないので「絞れてるつもり」で全部の束に反応してしまう事故が起きます。絞りたいならプログラムの中で tool_name を見て分岐させます
  • tool_responseがPostToolUseと同じ形だと思う。PostToolUseは整理済みのデータ、こちらはClaudeが見ているそのままの文字列です。Readならファイルの生の中身ではなく行番号つきテキストが来ます
  • プログラムがエラーで止まると、それだけでターンが止まる。このフックは終了コード2で終わると、次のモデル呼び出しの前に処理全体を止めます。「ツールを動かした直後になぜか毎回止まる」状態になったら、まずフックの終了コードを echo $? で疑ってください
  • 受け取る結果が巨大になることがある。公式も「Responses can be large, so parse only the fields you need.」と書いています。中身を丸ごと舐めず、必要な項目だけ読むのが安全です
  • continue:true を返せばターンが続くと思う。このイベントは decision: "block" を返すと、continueの指定に関わらずターンが終わります。止めたくないなら最初からblockを返さないことです
  • 理由を書かずにblockだけ返す。reasonを添えないと、理由なしの警告行だけが出て「なぜ止まったのか分からない」状態になります。止めるなら必ず理由を一緒に返してください

書き方

settings.jsonにPostToolBatchを登録する(matcherは書いても無視されるので書かない)。フックのプログラムには束で走った全ツールがtool_callsとして渡る

やってみるとこうなる

入力

Claudeに「accounts.pyとtransactions.pyを読んで重複処理を直して」と頼み、Readが2本並列で走る → その束が解決した直後にPostToolBatchが1回発火し、tool_callsにReadが2件入った状態で渡る

出力例

{
  "hookSpecificOutput": {
    "hookEventName": "PostToolBatch",
    "additionalContext": "These files are part of the ledger module. Run pytest before marking the task complete."
  }
}

このページに出てきた言葉

batch(バッチ)
Claudeが同時に走らせた処理の一束。Readを2本同時に動かしたら、その2本でひとつのbatch
並列
1個ずつ順番ではなく同時にまとめて走らせること。Claude Codeはファイルを複数読むときなどに自動でこれをやる
matcher
フックを特定のツールのときだけ動かすよう絞り込む条件。PostToolBatchでは書いても黙って無視される
tool_calls
束の中で走った全ツールを並べたデータ。各ツールに名前・入力・結果が入る
tool_response
ツールが返した結果。Readならファイルの生の中身ではなく行番号がついたテキストがそのまま入る
additionalContext
次にClaude本体へ問い合わせる前に1回だけ差し込まれる注意書きの文字列
終了コード2
フックのプログラムがこの値で終わると、次のモデル呼び出しの前にターンが止まる

関連項目

公式ドキュメント

https://code.claude.com/docs/en/hooks

-

← 戻る