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 以外だった時だけ additionalContext に tool_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を生成するブログ生成ツール