CLAUDE.mdにNEVER delete files in productionと書いたのに、長い作業の後半で守られなかった——Claude Codeを運用していると、必ずこの局面が来ます。
この記事では、なぜCLAUDE.mdのルールが「抜ける」のかの構造的な理由と、hookがなぜ確実に動くのかを整理します。
環境
- Claude Code(hooks対応バージョン)
- CLAUDE.md運用(4,696文字・115行規模)
- UserPromptSubmit / Stop / PreCompact hookを本番運用中
結論:ソフトウォールとハードウォールの違い
| 仕組み | どう機能するか | 信頼性 |
|---|---|---|
| CLAUDE.mdのルール記述 | LLMがコンテキストとして読む → 確率的に従う | ソフトウォール(コンテキストが長くなると抜けやすい) |
| hook(UserPromptSubmit/Stop等) | Claude Codeハーネスが実行する → 必ず動く | ハードウォール(LLMの判断と独立) |
CLAUDE.mdは「してほしいこと」の定義場所。hookは「必ずさせること」の実装場所。
なぜCLAUDE.mdは「ソフトウォール」なのか
CLAUDE.mdに書いたルールは、Claudeへのコンテキスト注入として機能します。LLMはそれを読み、「できる限り従おうとします」が、以下の条件で崩れます。
コンテキストウィンドウが長くなる: 会話が長くなるにつれ、CLAUDE.mdの内容が相対的に薄まります。直近の会話の方がLLMにとって重い文脈になります。
ルールの量が多い: 禁止事項が増えると、多数の判断が必要な場面で一部が見落とされやすくなります。
強調語の効力消失: NEVER、MUST NOT、CRITICALを多用すると、すべての記述が同じ重みになります。
なぜhookは「ハードウォール」なのか
hookはClaude Codeのハーネスが、定義されたライフサイクルポイントで実行します。Claudeのコンテキストに依存しません。
// settings.json の例
{
"hooks": {
"Stop": [
{
"hooks": [
{
"type": "command",
"command": "bash /path/to/git-status-check.sh"
}
]
}
]
}
}
このスクリプトは、Claudeが「実行するかどうか判断する」のではなく、ハーネスが強制的に呼び出します。LLMの判断が介在しません。
さらに、settings.jsonを編集するとhot reloadが効き、同じセッション内で即座に反映されます(「次セッションから有効」ではない)。
CLAUDE.mdに書くこと vs hookに移すこと
CLAUDE.mdに書く(LLMへのコンテキスト定義)
- プロジェクトの文脈定義(ターゲット・方針)
- 高レベルのアーキテクチャ方針
- 役割とスコープ
- 推奨パターン
hookに移す(「必ず起こさなければならないこと」)
| タイミング | hook種別 | 用途の例 |
|---|---|---|
| 毎プロンプト前 | UserPromptSubmit | git contextの自動注入、lintチェック |
| 作業完了後 | Stop | git statusの可視化、未pushの警告 |
| コンテキスト圧縮前 | PreCompact | 状態スナップショットの保存 |
判断の基準: 「1回でも抜けたら困るルール」は全部hookへ移す。
# Stop hookの例(git-status-check.sh)
#!/bin/bash
STATUS=$(git status --short)
UNTRACKED=$(git ls-files --others --exclude-standard | wc -l)
if [ -n "$STATUS" ] || [ "$UNTRACKED" -gt 5 ]; then
echo "⚠️ 作業後の状態確認"
echo "変更: $(echo "$STATUS" | wc -l)件 / untracked: ${UNTRACKED}件"
git status --short | head -20
fi
まとめ
CLAUDE.mdのルールが守られない原因は「書き方の問題」ではなく「仕組みの問題」です。hookを設計に組み込むことで、「また守られなかった」のループから抜け出せます。
AI ツールを業務に組み込む設計の実験ログを書いています。 https://masatoman.net