Claude Codeのフックで、自分のミスを自分で直させる設定
textlintで日本語の表記ゆれを直させ、テストが通るまで完了させない。Claude Codeのフックで自己修正の仕組みを作る方法を解説します。
Claude Codeに作業を任せると、最後に「完了しました」と丁寧に報告してくれます。ただ、その「完了」が本当に完了なのかは別の話です。テストが落ちたまま、表記がバラバラのまま、きれいな要約だけが返ってくることがあります。この記事では、フックという仕組みを使って、Claudeが自分のミスに自分で気づき、直してから報告するように設定する方法を紹介します。
フックとは何か
フックは、決まったタイミングでClaude Codeが自動で実行するコマンドです。プロジェクトの .claude/settings.json に一度書けば、あとは毎回必ず動きます。プロンプトに「必ずテストを実行してください」と書いても、それはお願いにすぎません。フックはお願いではなく、仕組みとして強制されます。
自己修正でよく使うイベントは次の4つです。名前は大文字小文字まで正確に書く必要があります。
PostToolUse:ファイル編集などのツール実行が成功した直後PostToolUseFailure:ツール実行が失敗した直後Stop:Claudeが返答を終えようとしたときSessionStart:セッションの開始、再開、/clear、コンパクト時
ポイントは「Claudeへの返し方」です。PostToolUse では終了コード2で終わると、標準エラーに書いた内容がそのままClaudeに届きます。Stop では {"decision": "block", "reason": "..."} を出力すると、Claudeは作業を続け、理由を読んで直しにいきます。SessionStart では標準出力がそのままコンテキストに入ります。
日本語ドキュメントの表記ゆれを自動で直させる
日本の現場でまず効くのは、コードよりも文章のチェックかもしれません。仕様書やヘルプページをClaudeに書かせると、「です・ます」と「だ・である」が混ざったり、「サーバ」と「サーバー」が揺れたりします。これは人が毎回指摘するより、textlint に任せたほうが確実です。
準備として、プロジェクトにtextlintと日本語向けのルールプリセットを入れます。
npm install --save-dev textlint textlint-rule-preset-ja-technical-writing
echo '{ "rules": { "preset-ja-technical-writing": true } }' > .textlintrc.json
次に .claude/hooks/check_ja.py を作ります。Claudeが編集したMarkdownだけをチェックし、問題があれば終了コード2で本人に突き返します。
#!/usr/bin/env python3
# 編集されたMarkdownをtextlintで確認し、指摘をClaudeに返す
import json, os, subprocess, sys
data = json.load(sys.stdin)
path = data.get("tool_input", {}).get("file_path", "")
if not path.endswith(".md"):
sys.exit(0)
os.chdir(os.environ.get("CLAUDE_PROJECT_DIR", "."))
r = subprocess.run(["npx", "textlint", path], capture_output=True, text=True)
if r.returncode != 0:
print("textlintの指摘があります。すべて直してから次に進んでください。\n" + r.stdout[-3000:], file=sys.stderr)
sys.exit(2)
Claudeは日本語の指摘文もきちんと読めるので、「文末表現が混在しています」といったエラーを受け取ると、その場で書き直します。人がレビューする前に、機械的な指摘はすべて消えている状態になります。
テストが落ちている間は「完了」させない
コードを扱うプロジェクトなら、Stopフックが一番効きます。.claude/hooks/finish_gate.py として保存してください。
#!/usr/bin/env python3
# 変更があるのにテストが落ちていれば、終了を一度だけ止める
import json, os, subprocess, sys
data = json.load(sys.stdin)
os.chdir(os.environ.get("CLAUDE_PROJECT_DIR", "."))
if not subprocess.run(["git", "status", "--porcelain"], capture_output=True, text=True).stdout.strip():
sys.exit(0)
t = subprocess.run(["npm", "test", "--silent"], capture_output=True, text=True)
if t.returncode == 0:
sys.exit(0)
if data.get("stop_hook_active"):
print(json.dumps({"systemMessage": "再試行後もテストが失敗しています。確認をお願いします。"}, ensure_ascii=False))
sys.exit(0)
print(json.dumps({"decision": "block",
"reason": "テストが失敗しています。原因を直し、npm testを再実行してから終了してください。\n" + (t.stdout + t.stderr)[-3000:]},
ensure_ascii=False))
stop_hook_active は、Claudeがすでにフックの指示で作業を続けているときにtrueになります。この確認を入れておくと、再試行は一度だけで、それでもだめなら人に戻ってきます。無限ループの心配がなくなります。
設定ファイルにまとめる
{
"hooks": {
"PostToolUse": [
{ "matcher": "Edit|Write",
"hooks": [{ "type": "command", "command": "python3 \"$CLAUDE_PROJECT_DIR/.claude/hooks/check_ja.py\"" }] }
],
"Stop": [
{ "hooks": [{ "type": "command", "command": "python3 \"$CLAUDE_PROJECT_DIR/.claude/hooks/finish_gate.py\"" }] }
],
"SessionStart": [
{ "matcher": "startup|resume|clear|compact",
"hooks": [{ "type": "command", "command": "cat \"$CLAUDE_PROJECT_DIR/.claude/lessons.md\" 2>/dev/null || true" }] }
]
}
}
最後のSessionStartは、.claude/lessons.md に書いた「過去の失敗から学んだルール」を毎回読み込ませるためのものです。compact を入れておくと、長い作業で会話が圧縮されたあとにも読み直されます。設定後はClaude Codeで /hooks と打ち、登録されているか確認しましょう。
学びは人が選んでから残す
失敗の記録から自動でルールを書き換えさせる方法もありますが、私はおすすめしません。誰も読まないルール集は、少しずつずれていきます。週に一度、次のように頼むくらいがちょうどいいです。
今週のgit logと差分を読み、同じ箇所を直し直した手戻りを探してください。
多かった原因を3つ挙げ、それぞれについて、.claude/lessons.md に追加する一行ルールを提案してください。
ファイルは20行以内に保ち、書き込む前に差分を見せて私の承認を待ってください。
数週間守られて効果があったルールは CLAUDE.md に移し、lessons.mdからは消します。
注意点
- フックはあなたの権限で動くシェルコマンドです。ネットで見つけたスクリプトも、必ず中身を読んでから登録してください。
- Stopフックは返答のたびに走ります。重いテスト全体ではなく、数十秒で終わる範囲に絞ってください。
- 一時的にすべて止めたいときは、設定に
"disableAllHooks": trueを入れます。
最初はtextlintのフックか、テストのStopフックのどちらか一つだけで十分です。「完了しました」の中身が変わるのを一度体験すると、他の作業にも広げたくなるはずです。イベントや出力形式の詳細は、公式のフックリファレンスにまとまっています。