PostToolUse(ポストツールユース)

フック
PostToolUse
ポストツールユース
Edit / Write / Bash などの道具が動き終わった直後に、好きなシェルスクリプトを自動で走らせる仕組み(hookと呼ばれる仕掛けの1種)。settings.json の hooks セクションに書いて使う

Claude Codeにファイル編集後の自動lintやテスト走行を任せたい人向け

Markdown を編集した直後に textlint で文体チェックを走らせて警告を AI 側に返したい場面、Bash でビルドが終わった直後に結果を AI に伝えたい場面、Write で新規ファイルが作られた直後に git add を済ませたい場面で、settings.json の hooks セクションに PostToolUse の枠を書いて使う

料理ブログを Hugo で作っているとき、AIに記事の Markdown を直してもらった直後、見出しレベルが飛んでないか・冒頭リードが薄くなってないかを毎回 textlint で検品したい場面があります。

毎回手で npx textlint を叩くのが面倒で、AI側に「編集したら自動でlintしてね」と頼んでも忘れる。

そこで使うのが PostToolUse です。Edit や Write が動き終わった瞬間に textlint を勝手に走らせて、警告が出たらその文面を AI 側に返して直させる役を任せられます。

噛み砕くと、料理を皿に盛った直後に立つ「盛付係」

料理人が皿に盛り終わったタイミングで、横から「汁が垂れてる」「彩りが寂しい」とメモを差し込む人を想像します。

料理を作り直すよう指示はできない、もう皿は出ているからです。代わりに「次の皿でこうして」と紙切れだけ渡せる。

これが PostToolUse の立ち位置です。Edit や Bash が動き終わった「直後」に呼ばれて、結果を見て、AI 側に追加の指示メモだけ返せる。

動いた手を巻き戻すことはできません。

画面に何が出るのか、settings.json と実行ログで見る

まず設定の置き場所はプロジェクト直下の .claude/settings.json。ここに hooks セクションを書きます。

{
  "hooks": {
    "PostToolUse": [
      {
        "matcher": "Edit|Write",
        "hooks": [
          {
            "type": "command",
            "command": "$CLAUDE_PROJECT_DIR/.claude/hooks/textlint-after-edit.sh"
          }
        ]
      }
    ]
  }
}

matcher の Edit|Write は「Edit が動いた時 か Write が動いた時」の両方に反応する書き方です。

AI が記事 Markdown を Edit すると、Claude Code は標準入力経由で次の JSON をシェルスクリプトに流し込みます。

{
  "session_id": "abc123",
  "hook_event_name": "PostToolUse",
  "tool_name": "Edit",
  "tool_input": {
    "file_path": "/cooking-blog/content/posts/keema-curry.md",
    "old_string": "## 材料",
    "new_string": "## 材料(2人分)"
  },
  "tool_response": {
    "filePath": "/cooking-blog/content/posts/keema-curry.md",
    "success": true
  },
  "tool_use_id": "toolu_01abc..."
}

シェルスクリプト側で tool_input.file_path を取り出し、textlint を走らせて、警告があれば additionalContext として返す流れです。

料理ブログで textlint を走らせる手順4ステップ

ステップ1: settings.json に PostToolUse の枠を書く

料理ブログのプロジェクト ~/cooking-blog/ の中に .claude/settings.json を作って、先ほどの hooks セクションを書き入れます。

matcher は Edit|Write。Markdown 編集が走ったら全部反応させたいので、両方を捕まえます。

Read や Glob には反応させません。読むだけなら lint は要らないからです。

ステップ2: textlint-after-edit.sh を作る

.claude/hooks/textlint-after-edit.sh を新規作成して、中身は次の通り。

#!/bin/bash
INPUT=$(cat)
FILE_PATH=$(echo "$INPUT" | jq -r '.tool_input.file_path')

# Markdown 以外なら無視
case "$FILE_PATH" in
  *.md) ;;
  *) exit 0 ;;
esac

# textlint を走らせる
LINT=$(npx textlint "$FILE_PATH" 2>&1 || true)

if [ -n "$LINT" ]; then
  jq -n --arg lint "$LINT" '{
    "hookSpecificOutput": {
      "hookEventName": "PostToolUse",
      "additionalContext": ("textlint warnings:\n" + $lint)
    }
  }'
fi
exit 0

標準入力で受け取った JSON から file_path を抜く、Markdown だけに絞る、textlint を走らせて警告があれば JSON で返す、これだけの流れです。

chmod で実行権限を付けるのを忘れないようにします。

$ chmod +x .claude/hooks/textlint-after-edit.sh

ステップ3: AI に Markdown を直させてみる

Claude Code に「content/posts/keema-curry.md の冒頭リードを2文で書き直して」と頼みます。

AI が Edit ツールでファイルを書き換えた直後、PostToolUse が発火して textlint-after-edit.sh が走ります。

もし textlint が「同じ語尾が3連続です」「一文が長すぎます」みたいな警告を出すと、その内容が additionalContext として AI 側に渡る。

ステップ4: 警告が AI に届いて、自動で直し直す

AI 側の画面では、Edit 完了直後に「textlint warnings: ...」というシステムリマインダーが差し込まれます。

AI はそれを読んで「あ、語尾が連続してた、書き直そう」と判断して、もう一度 Edit を打ち直す。

そのまた直後にまた textlint が走り、警告が消えていれば終了、残っていればまた直す、というループになります。

無限ループにならないよう、ステップ2のスクリプトには「warnings が空なら何も返さない」を入れているのがポイント。

仕組み早見表:input と output に何が入るか

標準入力で渡ってくる JSON のフィールド一覧です。

フィールド 中身
session_id string そのセッション固有のID
cwd string Claude Code を起動した時の作業フォルダ
hook_event_name string "PostToolUse" 固定
tool_name string 動いた道具の名前。Edit / Write / Bash など
tool_input object AI が道具に渡した内容(file_path とか command とか)
tool_response object 道具が返した結果。中身の形は道具ごとに違う。Write/Edit は {filePath, success}、Bash は {stdout, stderr, exit_code} など
tool_use_id string その道具呼び出し1回ぶんを区別するID
transcript_path string そのセッションの会話・ツール呼び出し履歴を保存しているファイルパス
permission_mode string その時点の権限モード(default / plan など)

標準出力で返せる JSON のフィールドはこちら。

フィールド 意味
continue true / false。false にすると Claude 側が処理を打ち切る
suppressOutput true にすると hook の stdout をトランスクリプトに残さない
systemMessage 会話画面にユーザー向け警告を出したい時に書く
hookSpecificOutput.hookEventName "PostToolUse" を指定
hookSpecificOutput.additionalContext AI 側に渡す追加メモ。textlint 警告などをここに入れる

matcher で指定できるツール名は次の通り。

  • ファイル編集系: Edit, Write, MultiEdit, NotebookEdit
  • 読み取り系: Read, Glob, Grep
  • 実行系: Bash
  • Web系: WebFetch, WebSearch
  • その他: TodoWrite, Task(サブエージェント起動), AskUserQuestion, ExitPlanMode
  • MCP 経由のツールは mcp__<サーバー名>__<ツール名> 形式(matcher は正規表現として評価される)

使いどころ3シナリオ

シナリオ1: Markdown 編集後に textlint で文体監査

料理ブログで「同じ語尾を3連続させない」ルールを守りたい時、毎回手でチェックは続かない。

matcher を Edit|Write、対象を *.md に絞って textlint を走らせる構成にします。

警告が出たら additionalContext 経由で AI に直接フィードバック、自動で直し直しさせる。

私は記事1本につき2〜3回はループが回る印象です。

シナリオ2: Bash でビルドコマンドが走った後、結果を Claude に伝える

hugo コマンドでサイトをビルドした直後、エラーや警告が出たかを AI に渡したい場面があります。

matcher を Bash にして、tool_response.exit_code が 0 以外だった時だけ additionalContexttool_response.stderr の中身を流し込む。

AI 側は「ビルドが通らなかった、原因はテンプレ参照ミス」と把握して、続けて修正に入ります。

毎回ターミナルログを目で追う作業が消える。

シナリオ3: Write で新規ファイルが作られた直後に git add

新しい記事ファイル content/posts/2026-04-21-keema.md が作られた瞬間、自動で git add しておきたい場面があります。

matcher を Write、シェルスクリプトで tool_input.file_path を抜いて git add "$FILE_PATH" を叩くだけ。

「あ、git add 忘れてた」がゼロになります。

私はこのパターンを一番気に入っています。

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

  • PostToolUse はツール実行を止められない。Edit が走る前に止めたいなら PreToolUse が正解。PostToolUse で decision: "block" を返しても効果はなく、ファイルはもう書き換わった後。AI 側にやり直しを促したいなら、hookSpecificOutput.additionalContext に文面を入れて返すか、exit 2 で stderr を出す(公式仕様で「stderr が Claude に表示される」と明記されている)。実行は巻き戻らない
  • exit code 2 を返してもブロックにならない。PreToolUse なら「2 を返すと止まる」が、PostToolUse は道具が動き終わった後なので、stderr が AI に表示されるだけです
  • Edit に反応する hook の中で Edit を呼ぶと無限ループになりかねない。私はやらかした経験あり。AI が Edit→hook 走る→hook が AI に「直して」と言う→AI がまた Edit、を延々繰り返します。終了条件(警告が空ならメッセージ返さない)を必ず入れる
  • tool_response が undefined のツールがある。AskUserQuestion や Agent 起動系は応答が得られない場合があります。スクリプト側で jq -r '.tool_response // empty' のように空対応を入れておくと安全
  • matcher の書き間違いEdit|Write は OR、"Edit" 単体は完全一致。"edit" と小文字で書いても効きません。MCP ツールは mcp__memory__.* のように正規表現で書く必要がある
  • 重い処理を入れると会話のレスポンスが遅くなる。Markdown 1ファイルに textlint 30秒、hook はデフォルトで 600秒待つので待ち時間がそのまま AI の応答待ちになります。重い処理は非同期で叩くか、対象ファイルを絞るかの工夫が要る
  • シェルスクリプトに実行権限を忘れるchmod +x し忘れて hook が静かに失敗するケースが多い。debug log にエラーは出ますが、表の会話画面には何も出ないので気づきにくい

関連するコマンド・ツールへの動線

  • PreToolUse - ツール実行のに走る方の hook。こちらは実行をブロックしたり、入力を書き換えたりできる。「Edit させたくない」「危険な Bash を止めたい」はこっち
  • Hooks - 親概念のページ。PostToolUse / PreToolUse / SessionStart など全7種類の hook イベントの一覧と全体像
  • settings.json - hooks セクションを書き込む先のファイル。プロジェクト直下とユーザー全体の2箇所がある
  • Edit - 既存ファイルの一部を置換する道具。PostToolUse の matcher で一番よく指定する対象
  • Bash - ターミナルでコマンドを叩く道具。ビルドやテスト後の検品で PostToolUse とよく組み合わせる

参考リンク

書き方

settings.json の "hooks" の中に "PostToolUse" 配列を作り、matcher(どの道具に反応するか)と command(走らせるシェルスクリプトのパス)を指定する

やってみるとこうなる

入力

{
  "hooks": {
    "PostToolUse": [
      {
        "matcher": "Edit|Write",
        "hooks": [
          {
            "type": "command",
            "command": "$CLAUDE_PROJECT_DIR/.claude/hooks/textlint-after-edit.sh"
          }
        ]
      }
    ]
  }
}

出力例

AI が Edit ツールで Markdown を書き換えた直後、textlint-after-edit.sh が標準入力経由で hook イベントの JSON を受け取り、textlint を走らせる。警告があれば標準出力に { "hookSpecificOutput": { "hookEventName": "PostToolUse", "additionalContext": "textlint warnings: ..." } } を返す。AI 側はその警告を読んで、もう一度 Edit して直す。

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

hook
Claude Code が特定のタイミング(ツール実行前後など)で自動的に呼び出してくれる、ユーザー側のシェルスクリプト
matcher
PostToolUse でどの道具に反応するかを指定する欄。<code>Edit|Write</code> のように <code>|</code> 区切りで複数書ける
tool_input
AI が道具に渡した内容を表す JSON。Edit なら file_path / old_string / new_string が入る
tool_response
道具が返した結果を表す JSON。exit_code / stdout / stderr / message などが入る。一部の道具では undefined になる場合がある
additionalContext
PostToolUse が AI 側に渡せる追加メモの欄。ここに lint 警告などを入れると、AI が次の応答でそれを読んで対応する
settings.json
Claude Code がプロジェクトごとの設定を読み込むファイル。<code>.claude/settings.json</code> として置く
標準入力 / 標準出力
プログラムの入り口と出口。hook では標準入力で JSON を受け取り、標準出力で JSON を返す
textlint
日本語/英語の文章を機械的にチェックして「冗長」「同じ語尾連続」などの警告を出すツール
Hugo
Markdown から静的HTMLを生成するブログ生成ツール

関連項目

公式ドキュメント

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

-

← 戻る