Claude Code を本格的に使い込んでいくと、必ずぶつかる壁があります。「CLAUDE.md にルールを書いたのに、Claude が無視してくる」問題です。
私も先月、自分の Astro サイトで prettier を必ず通すよう CLAUDE.md に明記していたのに、Claude Code がときどき素のまま書き出していて、git diff が荒れて困りました。これを解決するのが Hooks です。
この記事では、私が2週間運用して効果が大きかった Hooks の設定を、PreToolUse を中心にコード付きで紹介します。読み終える頃には、.claude/settings.json を書いて「絶対に防ぎたい事故」をブロックできるはずです。
結論:Hooks は CLAUDE.md の「お願い」を「契約」に変える仕組み
先に結論を言うと、Hooks は Claude Code のライフサイクル上の特定イベントで、シェルコマンドや HTTP エンドポイントを必ず実行させる仕組みです。
CLAUDE.md に書いたルールは確率的に守られるだけですが、Hooks は決定論的に動きます。Anthropic 公式ドキュメントでも、Hooks は「ユーザー定義のシェルコマンド、HTTP エンドポイント、または LLM プロンプトで、Claude Code のライフサイクル上の特定ポイントで自動実行される」と定義されています。
つまり、rm -rf を実行されたくない、.env を読まれたくない、Write した直後に必ず lint を通したい——こういう「絶対に外せないルール」を Hooks に任せて、CLAUDE.md は柔らかいガイドラインに専念させるのが2026年の正解です。
なぜ Skills や Subagents ではなく Hooks なのか
ここはよく混乱するポイントなので整理しておきます。私自身、最初は Skills と Hooks の使い分けがピンと来ませんでした。
Skills(SKILL.md)は「専門知識のロード」、サブエージェント(.claude/agents)は「タスクの分割並列実行」、そして Hooks は「動作の強制と監視」です。レイヤーが違うんですよね。
たとえば「TypeScript の編集後に必ず型チェックを通す」は、CLAUDE.md に書くと忘れられる、Skills に入れても発火タイミングは Claude 任せ。でも PostToolUse Hook なら、Write/Edit が成功した瞬間に100%走ります。LLM の判断を経由しないので、いわば「OSのシステムコール層に処理を差し込む」感覚に近いです。
Claude Code Hooks の全体像:イベントは何種類あるのか
ここは情報が錯綜していて、ブログによって「12イベント」「17イベント」「21イベント」「32+」と数字がバラバラです。理由は、バージョンの違いと「マッチャー違いを別カウントするか」の差です。
2026年5月時点の公式ドキュメント(v2.1.141 以降)では27の独立イベントが定義されていて、SessionStart のように startup/resume/clear/compact といったサブマッチャーを別カウントすると32以上になります。実務で押さえるべきは、ライフサイクルの5段階だけです。
- SessionStart / SessionEnd:セッション境界。コンテキストや環境変数の注入に使う
- UserPromptSubmit:ユーザー入力直後。プロンプト整形やスキル発火に
- PreToolUse / PermissionRequest:ツール実行直前のゲートキーパー
- PostToolUse / PostToolUseFailure:ツール実行後の自動処理
- Stop / PreCompact:応答終了・コンテキスト圧縮直前
このうち、最初に手を出すべきは PreToolUse と PostToolUse です。私の体感でも、効果の8割はこの2つに集中しています。
PreToolUse で危険コマンドを止める最小実装
まず一番効くやつから。rm -rf や git push --force のような不可逆コマンドを Bash ツール経由でブロックします。
.claude/settings.json にこう書きます。
{
"hooks": {
"PreToolUse": [
{
"matcher": "Bash",
"hooks": [
{
"type": "command",
"command": "node .claude/hooks/guard-bash.mjs"
}
]
}
]
}
}
そして .claude/hooks/guard-bash.mjs を作ります。
#!/usr/bin/env node
import { readFileSync } from 'node:fs';
const input = JSON.parse(readFileSync(0, 'utf-8'));
const cmd = input.tool_input?.command ?? '';
const denyList = [
/\brm\s+-rf\s+\//,
/\bgit\s+push\s+.*--force\b/,
/\b:\(\)\{\s*:\|:&\s*\};:/, // fork bomb
/\bcurl\s+[^|]+\|\s*(ba)?sh\b/,
];
for (const pattern of denyList) {
if (pattern.test(cmd)) {
console.error(`[BLOCKED] 危険なコマンドを検知: ${cmd}`);
process.exit(2);
}
}
process.exit(0);
ポイントは2つあります。
1つ目、入力は stdin の JSON で来ます。 環境変数ではありません。私も最初 $CLAUDE_TOOL_INPUT みたいなのを期待して詰まりました。実際にはそんな変数は存在せず、stdin から JSON を読むしかありません。
2つ目、ブロックしたいときの exit code は2です。 ここが落とし穴で、Unix の慣習で exit 1 と書いてしまうと「非ブロッキングのエラー」扱いになって、危険コマンドがそのまま通ります。ブログ The Prompt Shelf でも「ポリシー強制には必ず exit 2 を使え」と注意喚起されていて、ここを間違えるとフックが事実上機能しません。
PostToolUse でフォーマットと型チェックを強制する
次に効くのが、ファイル書き込み後の自動処理です。これは Anthropic 公式の文書化されたパターンで、Write|Edit にマッチしたら Prettier を走らせます。
{
"hooks": {
"PostToolUse": [
{
"matcher": "Write|Edit",
"hooks": [
{
"type": "command",
"command": "jq -r '.tool_input.file_path' | xargs -I{} sh -c 'npx prettier --write {} 2>/dev/null; npx tsc --noEmit 2>&1 | head -20'"
}
]
}
]
}
}
私はここに tsc --noEmit を足して、型エラーがあれば標準出力に出すようにしています。Hook の標準出力は Claude にフィードバックされるので、次のターンで Claude が自発的に型エラーを修正してくれるんですよね。これは想像以上に効きました。
ただし注意点が一つ。Hook の出力は10,000文字でキャップされます。 超えた分はファイル退避になり、プレビューと退避パスに置き換わる仕様です。tsc のフル出力をそのまま渡すと埋まるので、head -20 で切り詰めるか、ファイルにリダイレクトしてサマリだけ返すのが安全です。
非同期フックでログ収集を裏で回す
2026年1月のアップデートで、async: true フラグが追加されました。これは個人的にかなり気に入っている機能です。
{
"hooks": {
"PostToolUse": [
{
"matcher": "Bash|Write|Edit",
"hooks": [
{
"type": "command",
"command": "node .claude/hooks/audit-log.mjs",
"async": true,
"timeout": 30
}
]
}
]
}
}
async: true を付けると、Claude の応答を止めずにバックグラウンドで走ります。ロギング、Slack 通知、メトリクス送信のような「結果を待つ必要がない副作用」に向いています。
逆に、PreToolUse のセキュリティブロックや PermissionRequest の自動承認には絶対に async を使わないでください。判断を待たずに先に進んでしまうので、ブロックする意味が消えます。
クロスプラットフォーム対応の鉄則
私の Hooks スクリプトは全部 Node.js で書いています。理由は単純で、Claude Code は Node.js を必ず要求するので、node コマンドは macOS / Linux / Windows のどこでも確実に存在するからです。
やりがちなミスは cmd /c や bash -c を直接 settings.json に書いてしまうこと。これをやると、Windows で書いたフックが Linux のチームメイトの環境で全滅します。
安全策として、私はこのチェックリストを使っています。
- settings.json の
commandはnode script.mjsの形に統一 - スクリプト内でパスを組むときは
path.join()のみ。/や\を直書きしない $HOMEではなくos.homedir()、/tmpではなくos.tmpdir()- 改行コードはスクリプトから読む分には Node.js が吸収してくれるので気にしない
これを守るだけで、.claude/ をチームで git 共有してもトラブルがほぼ起きなくなります。
私が2週間運用して気づいた3つの注意点
最後に、ドキュメントを読んだだけでは見えなかった実運用の罠を共有します。
Stop フックで exit 2 を返すと無限ループになります。 Stop は Claude が応答を終えたいタイミングで発火しますが、exit 2 で止めると Claude は作業を継続します。これを条件なしで返すと永遠に止まりません。stop_hook_active フィールドを見て、2回目以降は素直に exit 0 する分岐を必ず入れてください。
shell の起動メッセージが JSON 出力を壊します。 .bashrc や .zshrc で何かを print している場合、Hook の stdout に混ざって JSON パースが失敗します。Hook 内で出力するときは、ログは stderr、構造化応答だけ stdout、と厳密に分けるのが安全です。
API キーは settings.json に平文で入ります。 MCP の env と同じく、Hook 設定ファイルは平文保存です。チーム共有する .claude/settings.json には機密を入れず、~/.claude/settings.json(ユーザー個別)側に置くか、allowedEnvVars で外部から渡すのが現実解です。
まとめ:今日から入れる3つの Hook
長くなったので、今日から手を動かすためのアクションを3つに絞ります。
- PreToolUse + Bash マッチャーで
rm -rf /とgit push --forceをブロック(上のコードをそのまま使えます) - PostToolUse + Write|Edit マッチャーで Prettier 自動実行(2分で入る、効果は無限)
async: trueでツール実行ログを JSONL で吐き出す(後でトラブルシュートに効く)
この3つを .claude/settings.json に入れた状態で1週間 Claude Code を走らせてみてください。CLAUDE.md だけで運用していた頃と比べて、「やらかし」の発生頻度がはっきり変わるはずです。
次のステップとしては、HTTP フックでチーム共通の検証サーバーを立てたり、Agent フックで「テストファイルとの対応をサブエージェントに確認させる」みたいな高度な使い方に進めます。ただ、まずはこの3つを血肉にしてからで十分です。
参考リンク
- Claude Code Docs — Hooks reference — 公式のイベント・JSON入出力仕様
- Claude Code Docs — Connect Claude Code to tools via MCP — MCP と Hooks の使い分け参考
- The Prompt Shelf — Claude Code Hooks Complete Reference 2026 — exit code 2 必須の落とし穴解説
- Claude Fast — Cross-Platform Hooks — Node.js 統一でのクロスプラットフォーム対応