プログラミング・スキルアップ

Claude Code Hooksの使い方|自動化フローの作り方2026

読了時間: 約26分

CLAUDE.mdに「コミット前に必ずテストを走らせること」と書いても、AIは3回に1回くらい忘れる。指示は確率で守られるからだ。Hooksはここを変える。決められたタイミングで必ずシェルコマンドを実行する仕組みで、LLMの判断を挟まない。忘れる余地がない。

本記事はClaude Codeのフックを実際に書いて動かした記録で、5本のスクリプトに計50件のペイロードを流している。終了コードを確認し、実行時間は約9ミリ秒の単位まで計測した。うまくいった話だけでなく、危険コマンドのブロックを2度すり抜けられた過程もそのまま載せる。rm -rfを弾く正規表現は、たいていの記事に載っているものが穴だらけだった。

この記事の要点

  • フックは.claude/settings.jsonに定義し、指定イベントで必ず実行される。プロンプトでの依頼と違い、実行が保証される
  • 制御は2系統。終了コード2で操作をブロックし、標準出力にJSONを返せば許可・確認・追加コンテキストまで指示できる
  • 実測したオーバーヘッドはbashで1回9ms、Pythonで26ms。ツール呼び出しのたびに走るため、常時発火させるフックはbashが有利
  • 正規表現による危険コマンドのブロックはrm -frrm -r -fで簡単にすり抜ける。フラグを分解して判定する必要がある

Claude Code Hooksは「お願い」を「保証」に変える仕組み

Claude Code Hooksとは、セッション中の特定のタイミングで自動的にシェルコマンドを実行する機能だ。ツールを使う直前、使った直後、応答が終わったとき。そうした区切りごとにイベントが発火し、登録しておいたコマンドが走る。

重要なのは、この実行にAIの判断が入らない点だ。CLAUDE.mdやプロンプトの指示は、モデルが読んで解釈して従うにすぎない。文脈が膨らめば優先度は下がる。急いでいれば飛ばされる。フックは違う。アプリケーション側の処理だから、条件に合えば100%走る。

たとえるなら、CLAUDE.mdは新人に渡す業務マニュアルで、Hooksは入館ゲートだ。マニュアルは読まれないことがあるが、ゲートは物理的に通れない。

よくある誤解

「フックを設定すればAIがルールを覚える」わけではない。フックはAIの外側で動く監視役だ。AIの振る舞い自体は変わらず、望ましくない結果だけが止められる。ルールを教えたいならCLAUDE.md、守らせたいならフック、と役割を分けて考えるとよい。

プロンプト・CLAUDE.md・フックの使い分け

3つの手段は競合しない。それぞれ効く場面が違う。

手段 実行の確実性 向いている用途
プロンプト その場限り 一度きりの指示
CLAUDE.md 確率的(文脈次第で薄れる) コーディング規約、設計方針、文体
Hooks 決定的(必ず走る) フォーマット、テスト実行、危険操作の遮断、監査ログ

判断が要ることはAIに任せ、判断が要らないことはフックに任せる。この線引きが実務では効く。blackprettierをかけるのに知性は不要だ。毎回同じ処理をするなら、毎回同じ処理をする仕組みに任せたほうが速いし確実だ。

フックイベント一覧と発火タイミング

イベントは30種類以上ある。日本語の解説記事では「全14イベント」「18イベント」と書かれているものが多いが、公式リファレンスの一覧はすでにその倍近くまで増えている。サブエージェント、タスク管理、worktree、MCPのエリシテーションまでフック対象になった。

イベント名と発火条件の一覧は公式のHooksリファレンスにまとまっている。とはいえ全部を覚える必要はない。実務で使うのは最初の6つでほぼ足りる。残りは「そういうものがある」と知っておいて、必要になったときリファレンスを引けばいい。

まず押さえる6イベント

イベント 発火タイミング ブロック 典型的な用途
PreToolUse ツール実行の直前 危険コマンドの遮断、入力の書き換え
PostToolUse ツール実行の成功後 不可 フォーマッタ、Lint、構文チェック
UserPromptSubmit プロンプト送信直後、処理前 ブランチ名など文脈の自動注入
Stop Claudeが応答を終えたとき 完了通知、やり残しチェック
SessionStart セッション開始・再開時 不可 環境変数の読み込み、初期情報の注入
Notification Claude Codeが通知を出すとき 不可 デスクトップ通知、Slack転送

残り25以上のイベントは何を捕まえるのか

用途ごとに固まっている。眺めておくと「あの処理も自動化できる」と気づける。

ツールまわりの細分化

PostToolUseFailureは失敗時のみ、PostToolBatchは並列ツール呼び出しが全部片付いたあとに走る。失敗だけをログに残す用途に向く。

権限の判定

PermissionRequestは権限確認が必要になった瞬間、PermissionDeniedは自動判定で拒否された瞬間に発火する。

サブエージェントとタスク

SubagentStart / SubagentStopTaskCreated / TaskCompleted。サブエージェントの実行時間計測に使える。

環境の変化

CwdChangedFileChangedConfigChangeDirectoryAdded。設定ファイルが書き換わったことを検知して監査ログに残せる。

コンテキスト圧縮

PreCompact / PostCompact。圧縮で消えた前提条件を、圧縮直後に再注入する使い方が効く。

worktreeとMCP

WorktreeCreate / WorktreeRemoveElicitation。MCPサーバーがユーザー入力を求めた瞬間も拾える。

ブロックできるイベントは限られる

終了コード2で処理を止められるのはPreToolUseUserPromptSubmitPermissionRequestStopSubagentStopPreCompactConfigChangeなどブロック可能とされたイベントだけだ。PostToolUseで2を返しても処理は止まらない。すでに実行された後だから当然だが、ここは最初に間違えやすい。

Claude Code Hooksの設定ファイルと配置場所の優先順位

設定の書式は公式のフック入門ガイドに例がある。フックの定義はhooksキーの下に、イベント名 → マッチャーの配列 → 実行するフックの配列、という3階層で書く。この入れ子が一段深いので、最初はコピーして形を覚えるのが早い。

{
  "hooks": {
    "PreToolUse": [
      {
        "matcher": "Bash",
        "hooks": [
          {
            "type": "command",
            "command": "$CLAUDE_PROJECT_DIR/.claude/hooks/block-dangerous.py",
            "timeout": 5
          }
        ]
      }
    ],
    "PostToolUse": [
      {
        "matcher": "Edit|Write",
        "hooks": [
          {
            "type": "command",
            "command": "$CLAUDE_PROJECT_DIR/.claude/hooks/format-after-edit.sh"
          }
        ]
      }
    ],
    "Stop": [
      { "hooks": [ { "type": "command", "command": "notify-send 'Claude Code 完了'" } ] }
    ]
  }
}

この設定は実際にパースを確認し、3件のフック定義として正しく読めた。PreToolUseはmatcherがBashPostToolUseEdit|Writeの2ツール、Stopはmatcher省略で全対象という3グループとして読める。matcherは正規表現で書けて、大文字小文字は区別される。

フックの各フィールド

フィールド 意味
type command / http / mcp_tool / prompt / agent の5種類
command 実行するシェルコマンド
args 非空の配列にするとexec形式になり、シェル展開が起きない
if ツール名だけでなく引数まで見て絞る。Bash(git *)のような書き方
timeout 秒。既定はcommand/http/mcp_toolが600秒、promptが30秒、agentが60秒
async trueで非同期実行。待たせたくない通知系に使う

ifを足すだけで起動回数は変わる。matcherがBashだとすべてのシェル実行でフックが起動するが、"if": "Bash(git *)"と書けばgitコマンドのときだけに絞れる。起動が減れば、後述の約9ミリ秒や約26ミリ秒の積み上げも比例して減る。

どのファイルに書くか

同じ設定キーが複数の場所にあると、優先順位の高いほうが勝つ。上から強い順に並べるとこうなる。

配置場所 スコープ 共有
組織の管理設定(MDM等) 全社 強制
CLIの起動引数 その起動のみ なし
.claude/settings.local.json 自分だけ・このリポジトリ gitignore対象
.claude/settings.json チーム・このリポジトリ コミットして共有
~/.claude/settings.json 自分の全プロジェクト なし

チームで守らせたいルールは.claude/settings.jsonに置いてコミットする。これがフックの本領だ。設定ファイルの使い分けと同じ発想で、個人の好みはsettings.local.jsonに逃がす。

設定を書き換えたあとセッションを再起動する必要はない。ファイルウォッチャーが数秒で拾う。反映されたかどうかは/hooksで確認できる。ただし/hooksは閲覧専用のメニューで、そこから編集はできない。イベント名・matcher・type・そこで走るコマンドが一覧で見えるだけだ。

5分で動く最初の1本を作る

仕様を読むより、1本動かすほうが早い。Pythonファイルを編集したら構文チェックを走らせるフックを作る。

#!/usr/bin/env bash
# .claude/hooks/format-after-edit.sh
file=$(cat | jq -r '.tool_input.file_path // ""')
case "$file" in
  *.py) [ -f "$file" ] && python3 -m py_compile "$file" && echo "構文チェックOK: $file" ;;
  *) exit 0 ;;
esac

フックは標準入力からJSONを受け取る。jq -rで必要なフィールドだけ抜き出すのが定石だ。// ""を付けておくとフィールドが無いときも空文字になり、スクリプトが落ちない。

設定に登録する前に、手元で単体テストできる。これが一番の時短になる。Claude Codeを起動せず、想定するペイロードをそのまま流し込めばいい。

$ echo '{"hook_event_name":"PostToolUse","tool_name":"Edit",
  "tool_input":{"file_path":"src/site_db.py"},
  "tool_response":{"success":true}}' | bash .claude/hooks/format-after-edit.sh
構文チェックOK: src/site_db.py
exit code = 0

$ echo '{"hook_event_name":"PostToolUse","tool_name":"Edit",
  "tool_input":{"file_path":"dist/index.html"}}' | bash .claude/hooks/format-after-edit.sh
exit code = 0   # 出力なしで素通り

実行結果はこのとおりで、.pyだけ拾って.htmlは素通りした。ここまで確認してからsettings.jsonに登録すれば、動かないときの原因切り分けが「設定の書き方」だけに絞られる。

標準入力のテストを先にやる

フックが動かないという相談の大半は、スクリプト側のバグと設定ミスが混ざって切り分けられなくなっている状態だ。echo '...' | bash your-hook.shを先に通しておけば、この混乱は起きない。

ペイロードに何が入っているか

どのイベントでも共通して入るのがsession_idtranscript_pathcwdpermission_modehook_event_nameだ。ここにイベント固有のフィールドが乗る。

イベント 固有フィールド
PreToolUse / PostToolUse tool_nametool_input
UserPromptSubmit prompt
SessionStart / PreCompact source(startup / resume / clear / compact など)
Stop stop_hook_active
SessionEnd reason(clear / resume / logout など)
FileChanged file_pathevent(create / modify / delete)

exit codeとJSON出力、2系統の制御方法

フックからClaude Codeへの伝達手段は2つある。終了コードで大まかに伝えるか、標準出力にJSONを返して細かく指示するか。用途で選ぶ。

終了コードによる制御

終了コード 挙動
0 成功。UserPromptSubmitSessionStartでは標準出力がそのままコンテキストに追加される
2 ブロック。標準エラー出力の内容がClaudeへのフィードバックになる
その他 非ブロックのエラー。処理は続き、エラーが報告される

2を返すときは必ず標準エラー出力に理由を書く。ここに書いた文字列がClaudeに渡り、次の行動の材料になるからだ。沈黙は最悪手だ。無言でブロックすると、Claudeは同じ操作を再試行してまた弾かれる。

$ echo '{"hook_event_name":"PreToolUse","tool_name":"Bash",
  "tool_input":{"command":"rm -rf dist/"}}' | bash block-dangerous.sh
ブロック: 再帰削除コマンドは禁止されています -> rm -rf dist/
exit code = 2
所要時間 = 13 ms

JSON出力による制御

もっと細かく指示したいときは、標準出力にJSONを返す。表現力はこちらが上だ。終了コードが0でもJSONは常に解釈されるため、許可・確認・入力の差し替えまで一度に伝えられる。

#!/usr/bin/env python3
import json, sys

payload = json.load(sys.stdin)
cmd = payload.get("tool_input", {}).get("command", "")

if cmd.startswith("git push"):
    out = {"hookSpecificOutput": {
        "hookEventName": "PreToolUse",
        "permissionDecision": "ask",
        "permissionDecisionReason": "push は内容を確認してから実行してください"}}
else:
    out = {"hookSpecificOutput": {
        "hookEventName": "PreToolUse",
        "permissionDecision": "allow"}}

print(json.dumps(out, ensure_ascii=False))

permissionDecisionにはallowdenyaskdeferが指定できる。askは通常の確認プロンプトを出す指示で、上のスクリプトはgit pushのときだけ人間に判断を戻している。

さらにupdatedInputを返せば、ツールの入力そのものを差し替えられる。greprgに置き換える、といった矯正が書ける。additionalContextならブロックせず補足情報だけ渡せる。

全イベント共通のJSONフィールド

フィールド 効果
continue: false Claudeを完全に停止させる(ブロック可能イベントのみ)
stopReason 停止時に表示する理由
systemMessage 止めずに警告だけ出す

フックは権限を緩められない

ここは見落としがちだが重要だ。permissionDecision: "allow"を返しても、denyルールや組織の管理設定によるブロックは解除されない。フックができるのは権限を厳しくする方向だけだ。「フックで全部自動承認して確認を消す」という使い方は、そもそも仕様上できない。

危険コマンドのブロックは2度すり抜けられた

フックの代表的な用途がrm -rfの遮断だ。多くの記事に正規表現の例が載っている。9件のコマンドで検証したところ、最初の実装は止めるべき5件のうち4件を取り逃した。

1回目: よく見る正規表現

cmd=$(echo "$payload" | jq -r '.tool_input.command // ""')
if echo "$cmd" | grep -qE 'rm[[:space:]]+-[a-zA-Z]*r[a-zA-Z]*f'; then
  echo "ブロック: 再帰削除コマンドは禁止されています -> $cmd" >&2
  exit 2
fi
exit 0

3ケース流した結果がこうだ。

rm -rf dist/     -> exit 2  ブロック成功
ls -la dist/     -> exit 0  正しく通過
rm -fr /tmp/x    -> exit 0  すり抜け

rm -frが通ってしまう。rの後にfが来る前提の正規表現だからだ。フラグの順番が逆でも動作は同じなのに、検知は外れる。

2回目: 順番の逆を足す

なら逆順も足せばいい。--recursive--forceのロング形式も追加した。6件のコマンドで再検証した結果がこうなった。

rm -rf dist/                        -> exit 2  ブロック
rm -fr /tmp/x                       -> exit 2  ブロック
rm -r -f build                      -> exit 0  すり抜け
rm --recursive --force node_modules -> exit 2  ブロック
rm dist/old.html                    -> exit 0  正しく通過
ls -la                              -> exit 0  正しく通過

今度はrm -r -f buildが抜ける。フラグを分けて書かれると、1つのトークンにrfが同居する前提が崩れるからだ。パターンを足すたびに別の書き方で抜けられる。正規表現でシェルコマンドを判定するアプローチ自体に無理がある。

3回目: フラグを分解して判定する

文字列マッチをやめて、コマンドをトークンに割ってフラグ集合を作る方式に変えた。-rfのような結合短縮形は1文字ずつバラす。

#!/usr/bin/env python3
import json, shlex, sys

payload = json.load(sys.stdin)
cmd = payload.get("tool_input", {}).get("command", "")

try:
    tokens = shlex.split(cmd)
except ValueError:
    tokens = cmd.split()

flags, saw_rm = set(), False
for t in tokens:
    if t == "rm":
        saw_rm = True
        flags.clear()
        continue
    if not saw_rm:
        continue
    if t.startswith("--"):
        flags.add(t.lstrip("-"))
    elif t.startswith("-"):
        flags.update(t[1:])          # -rf を r と f に展開

recursive = "r" in flags or "R" in flags or "recursive" in flags
force = "f" in flags or "force" in flags

if saw_rm and recursive and force:
    print(f"ブロック: rm の再帰強制削除は禁止です -> {cmd}", file=sys.stderr)
    sys.exit(2)
sys.exit(0)

9件のコマンドで再検証した。今度は全部意図どおりに動いた。

rm -rf dist/                        -> exit 2  ブロック
rm -fr /tmp/x                       -> exit 2  ブロック
rm -r -f build                      -> exit 2  ブロック
rm --recursive --force node_modules -> exit 2  ブロック
rm -Rf dist                         -> exit 2  ブロック
rm dist/old.html                    -> exit 0  通過
rm -f dist/old.html                 -> exit 0  通過
git rm -r --cached .                -> exit 0  通過
ls -la                              -> exit 0  通過

-Rの大文字も拾えている。git rm -r --cached .はインデックスからの除去でファイルは消えないので、通してよい。forceが立っていないため自然に通過した。

それでも完全ではない

この方式も万能ではない。alias経由、変数展開、find -deleteのような別コマンドは素通りする。フックは最後の砦ではなく、うっかりを減らす層だと考えたほうがいい。本当に消えて困るものは、権限設定のdenyルールとバックアップで守る。

hooks 設定のオーバーヘッドを実測する

フックはツール呼び出しのたびに走る。1回の遅延は小さくても、長いセッションでは積み上がる。実際に測った数字は、約9ミリ秒と約26ミリ秒だった。同じ処理をbashとPythonで書き、それぞれ20回実行して1回あたりの平均を計測した結果、差は約3倍になった。

実装 1回あたり ツール200回のセッションでの合計
bash + jq 9ms 約1.8秒
Python 3.11 26ms 約5.2秒

差は約3倍。Pythonインタプリタの起動コストがそのまま出ている。単発なら誤差だが、全ツール呼び出しに掛けるフックでは効いてくる。

とはいえ26msを嫌ってロジックを削るのは本末転倒だ。前章の判定はPythonだから正しく書けた。使い分けの基準はシンプルで、常時発火するフックはbash、条件を絞ったフックはPythonでよいifフィールドでBash(rm *)のように絞り込めば、Pythonでも発火回数は激減する。

通知やSlack投稿のように結果を待つ必要がない処理は"async": trueにする。待ち時間がゼロになる。

実務で効くフックのレシピ

効くのはこの3つだ。派手な自動化ではなく、編集のたびに発生する手作業を消すものが効く。

1. 編集後の自動整形

PostToolUse + Edit|Write。差分にフォーマット揺れが混ざらなくなり、レビューが読みやすくなる。

2. 作業文脈の自動注入

UserPromptSubmitで現在のブランチや変更ファイル数を渡す。毎回「今どのブランチ?」と聞かれずに済む。

3. 長時間タスクの完了通知

Stopでデスクトップ通知。別作業に移っていても終了に気づける。async推奨。

ブランチ名をプロンプトに自動で添える

#!/usr/bin/env bash
# .claude/hooks/inject-context.sh  (UserPromptSubmit)
branch=$(git rev-parse --abbrev-ref HEAD 2>/dev/null || echo "not-a-repo")
changed=$(git status --porcelain 2>/dev/null | wc -l)
echo "現在のブランチ: $branch / 未コミットの変更: ${changed}件"
exit 0

UserPromptSubmitで終了コード0のとき、標準出力はそのままコンテキストに入る。echoするだけで文脈を足せるので、いちばん費用対効果が高いフックだと思う。

設定ファイルの変更を監査ログに残す

#!/usr/bin/env bash
# .claude/hooks/audit-config.sh  (ConfigChange)
payload=$(cat)
src=$(echo "$payload" | jq -r '.source // "unknown"')
file=$(echo "$payload" | jq -r '.file_path // "unknown"')
printf '%s\t%s\t%s\n' "$(date -Iseconds)" "$src" "$file" \
  >> "$CLAUDE_PROJECT_DIR/.claude/config-audit.log"
exit 0

$CLAUDE_PROJECT_DIRはプロジェクトルートを指す環境変数だ。フックはカレントディレクトリが変わる可能性があるので、パスはこの変数を起点に書く。相対パスで書いて動かなくなるのは、よくある失敗のひとつ。

Routinesや自動化パイプラインと組み合わせる

フックはRoutinesのような定期実行と相性がいい。人が見ていない実行では、品質チェックの失敗を検知して止める役が要る。Stopフックでチェックスクリプトを走らせ、基準に届かなければdecision: "block"を返して続行させる、という設計が組める。

ただしStopフックの連続ブロックには上限がある。8回連続でブロックするとClaude Codeが打ち切る。無限ループを防ぐ安全装置で、CLAUDE_CODE_STOP_HOOK_BLOCK_CAPで変更できる。

つまずきポイントとデバッグ手順

動かないときに疑う順番は決まっている。上から潰していけば、たいてい数分で原因にたどり着く。

フックが発火しない

  • matcherの綴りと大文字小文字を確認する。bashではBashにマッチしない
  • イベント名が正しいか。PostToolUsePostToolUserと書いても警告は出ない
  • /hooksで一覧に出ているか確認する。出ていなければ設定が読まれていない
  • 実行権限があるか。chmod +xの付け忘れは定番
  • disableAllHooksが有効になっていないか

JSON出力が効かない

これはシェルの初期化ファイルが原因のことがある。~/.bashrc~/.zshrcに無条件のechoが入っていると、その出力がフックの標準出力に混ざってJSONが壊れる。「設定は正しいのに解釈されない」ときは真っ先に疑う場所だ。

# ~/.bashrc の対話シェル以外での出力を止める
if [[ $- == *i* ]]; then
  echo "ようこそ"
fi

複数フックが競合する

同じイベントに複数のフックを登録すると並列に実行される。実行順は保証されない。updatedInputで同じツールの入力を書き換えるフックを2本置くと、最後に終わったほうが勝つ。どちらが最後になるかは決まらないので、入力を書き換えるフックは1つのイベントに1本までにする。

タイムアウトで途中終了する

既定のタイムアウトはcommand型で600秒あるが、UserPromptSubmitは30秒、MessageDisplayは10秒が上限になっている。プロンプト送信時にテストスイート全体を走らせるような設計は、ここで頭を打つ。重い処理はPostToolUseStopに寄せる。

セキュリティの前提

公式ドキュメントのセキュリティに関する記述でも警告されているとおり、フックに書いたコマンドは自分の権限でそのまま実行される。他人のリポジトリをクローンして開くとき、.claude/settings.jsonにフックが仕込まれていれば、それも動く。MCPサーバーの選定と同じ話で、信頼できないリポジトリの設定ファイルは開く前に中身を見る。

よくある質問

Q. Hooksを使うのに追加料金はかかりますか?

かかりません。フックはローカルでシェルコマンドを実行する機能で、API呼び出しは発生しません。ただしtype: promptやtype: agentのフックはモデルを呼ぶため、その分のトークンは消費されます。command型なら課金への影響はゼロです。

Q. 設定を変えたらClaude Codeを再起動する必要がありますか?

不要です。ファイルウォッチャーが数秒で拾います。

Q. PostToolUseで終了コード2を返せば、実行を取り消せますか?

取り消せません。PostToolUseはツールがすでに実行された後に発火するイベントで、ブロック対象外です。標準エラー出力の内容はユーザーに表示されますが、処理そのものは続行します。実行前に止めたい場合はPreToolUseを使ってください。

Q. フックで権限確認をすべて自動承認できますか?

できません。permissionDecisionにallowを返しても、denyルールや組織の管理設定によるブロックは解除されない仕様です。フックが権限に対してできるのは厳しくする方向だけで、緩めることは設計上できません。

Q. チームメンバー全員に同じフックを適用するにはどうしますか?

.claude/settings.jsonにフックを定義してリポジトリにコミットします。個人ごとの設定は.claude/settings.local.jsonに書き、こちらはgitignoreに入れます。優先順位はlocalのほうが高いため、個人設定でチーム設定を上書きできます。

Q. フックスクリプトはbashとPythonのどちらで書くべきですか?

迷ったらbashです。全ツール呼び出しで発火するフックの実測値は1回あたりbashが9ms、Pythonが26msで、約3倍の差がつきました。ただしコマンドの構文解析のように判定が複雑な場合は、Pythonのほうが正しく書けます。ifフィールドで発火条件を絞り込めば、Pythonを選んでも実行回数そのものを大きく減らせるため、速度差は実用上ほとんど問題になりません。

まとめ

Hooksの価値は、AIへの依頼を仕組みに変えられる点にある。CLAUDE.mdに書いたルールは守られたり守られなかったりするが、フックは必ず走る。この差は、人が見ていない自動実行になるほど大きくなる。

導入するなら順番はこうだ。まずPostToolUseで自動整形を1本入れる。標準入力にJSONを流す単体テストを必ず先に通す。慣れたらUserPromptSubmitで文脈注入、最後にPreToolUseのガードへ進む。ガードを最初に作ると、正しく動いているのか判断できないまま設定だけが増える。

危険コマンドの遮断は、正規表現をやめてフラグを分解する。rm -rfしか見ない実装は甘い。rm -frrm -r -fで簡単に抜けられる。9件のコマンドで比べたところ、最初の実装が止められたのは危険な5件のうち1件だけだった。完全な防壁にはならない。それでも、うっかりを減らす層としては十分に働く。

.claude/hooks/format-after-edit.shを1本置く。5行で足りる。Skillsやペルソナ設定と組み合わせれば、指示待ちのツールが、決めた手順を必ず踏む開発環境に変わる。