本文へスキップ
もと @moto_ai_jp

第4部 ハーネス · 第9回 / 全20回 · 15分くらい

守らせたいことは、読ませずに機械で止める(フック)

第3世代、ハーネスです。実践編ではまず、AIの作業の前後に自分のコードを差し込む「フック」を扱います。ことばの歯止めを、必ず効く歯止めに変える道具です。ただし、フックにも「時間切れになると止めずに通してしまう」といった落とし穴があります。そこまで含めて、ドキュメントで確かめます。

  1. 1
  2. 2
  3. 3
  4. 4
  5. 5
  6. 6
  7. 7
  8. 8
  9. 9
  10. 10
  11. 11
  12. 12
  13. 13
  14. 14
  15. 15
  16. 16
  17. 17
  18. 18
  19. 19
  20. 20

履修 0 / 20 回

この回を読み終えると、できるようになること

  1. フックの主なイベントと、終了コード0・2の意味を説明できる
  2. 危ないコマンドを止めるフックを、入力の受け取り方から書ける
  3. フックの時間切れが、イベントによってどう扱われるかを説明し、それに合わせた置き場所を選べる

専門用語は、初めて出たところで括弧に短い説明を付けています。説明が長くなるものには ※番号を付け、ページの終わりの用語メモにまとめました。

1フックとは何か

フック※1は、Claude Code の決まったタイミングで、決まったコマンドを必ず走らせる仕組みです。AIが忘れても、気が変わっても、コマンドは毎回走ります。設定ファイル(.claude/settings.json など)に書きます。

イベントいつ走るかよく使う用途
PreToolUseAIが道具を使う直前危ない操作を止める
PostToolUse道具を使った直後整形・型の確認・速いテスト
UserPromptSubmit自分が頼みごとを送った直後前提の差し込み、記録
StopAIが応答を終えるとき終わる前の最終確認
SessionStart / SessionEnd会話の始まりと終わり案内の表示、後片付け
🛠️作る🔍確かめる自動で合否✓ 合格 → 出す✗ 不合格理由を読んで直し、もう一度3回だめなら止まって報告
鍵は2つ。確かめるのを自動にすることと、落ちた理由をAIが読める形で残すことです。

2入力を受け取り、終了コードで答える

フックのコマンドは、何が起きようとしているかを JSON で標準入力※2から受け取ります。PreToolUse なら、使おうとしている道具の名前と、その引数(Bash なら実行しようとしているコマンド)が入っています。

答え方は、終了コードで決まります。

  • 0:問題なし。そのまま進める
  • 2:止める。PreToolUse なら道具の実行が止まり、標準エラー出力※3に書いたメッセージがAIに渡る。AIはそれを読んで、別のやり方を考える

止めたときにメッセージを返せるのが大事なところです。「消すな」と書いておくより、消そうとした瞬間に「この理由で止めた」と返すほうが、AIは次の手を正しく選べます。より細かく答えたいときは、JSON で permissionDecision: "deny" を返す方法もあります。

// .claude/settings.json(例)
{
  "hooks": {
    "PreToolUse": [
      {
        "matcher": "Bash",
        "hooks": [{ "type": "command", "command": "~/.claude/hooks/guard.py" }]
      }
    ]
  }
}

3止める条件は、コードで書く

フックの中身はただのスクリプトです。受け取ったコマンドが決めた形に当たれば止め、当たらなければ通します。判断がいらない決まりごとはここに置き、判断が要るものはAIに残します。

下の例は、自分が実際に事故を起こした形を止めるものです。読むだけのつもりで原本を上書きするコマンド、定時実行の登録をリンクで置く操作、元に戻せない git の操作などです。

#!/usr/bin/env python3
# ~/.claude/hooks/guard.py:PreToolUse で、危ない形の Bash だけを止める
import json, re, sys

cmd = json.load(sys.stdin).get("tool_input", {}).get("command", "")
RULES = [
    (r"plutil\s+-extract(?!.*\s-o\s)", "plutil -extract は -o - を付けないと原本を上書きします"),
    (r"\bln\s+-s\b.*LaunchAgents", "LaunchAgents には実体のファイルを置きます(リンクだと原本と一緒に壊れる)"),
    (r"git\s+push\s+.*--force|git\s+reset\s+--hard", "元に戻せない git の操作は、確認してから行います"),
]
for pattern, why in RULES:
    if re.search(pattern, cmd):
        print(why, file=sys.stderr)   # このメッセージがAIに渡る
        sys.exit(2)                    # 2 = 止める
sys.exit(0)

読み方:json.load(sys.stdin) でフックへの入力を読み、tool_input.command から実行しようとしているコマンドを取り出しています。re.search は、決めた形(正規表現)に当たるかの判定です。

4権限の設定でも止められる

止めたいものが「このファイルを読ませない」「このコマンドは許さない」のように単純なら、フックを書かなくても、設定ファイルの permissions.deny(許さない操作の一覧)で止められます。ドキュメントの例では、"Read(./.env)" のように書いて、鍵の入ったファイルを読ませないようにしています。

形が単純なら権限の設定、条件が複雑なら(「-o が付いていないときだけ」など)フック、と使い分けます。どちらも、AIの判断を通らない、必ず効く歯止めです。

5落とし穴1:時間切れになると、止めずに通す

フックには時間の上限があります。ドキュメントによると、コマンドのフックの既定は600秒で、頼みごとを送るたびに走る UserPromptSubmit は30秒です。

見落としやすいのは、PreToolUse のフックが時間切れになると、道具の実行は止まらない、という点です。止める役のフックが重くて時間切れになると、そのまま通してしまいます。安全側に倒れない作り(fail-open※4)なので、止める役のフックは、軽く、速く書いておく必要があります。

6落とし穴2:会話の終わりのフックは、ほとんど時間がない

SessionEnd(会話の終わり)のフックは、ドキュメントによると全部で1.5秒の持ち時間しかありません。個別に時間を延ばす設定をしても、最大60秒までです。

自分の環境では、会話が終わるたびに、その会話をローカルのAIで要約するフックを置いていました。要約には2〜3分かかるので、ほぼ毎回途中で打ち切られていました。記録を数えると、要約を始めた回が2,416回あったのに、保存までたどり着いたのは27回だけでした。エラーは一度も出ていません。

時間のかかる処理は、フックの中でやらずに「やることの一覧に1行書くだけ」にして、本体は別の仕組み(定時実行など)に任せます。

7フックそのものも確かめる

フックは毎回走るので、遅いと作業全体が遅くなります。重いテストは PostToolUse ではなく Stop に置いて、作業の終わりに1回だけ走らせる、のように置き場所を選びます。

また、フックが壊れると、全部が止まるか、全部が素通りするかのどちらかになります。止めたい形・止めたくない形をいくつか用意して、フックに流して確かめてから入れます(演習1)。

誤解よくある思い違い

フックを入れれば、その操作は必ず止まる

PreToolUse のフックは、時間切れになると止めずに通します。止める役のフックは、軽く速く書きます。

会話の終わりに、まとめの処理をフックで走らせればよい

SessionEnd のフックは全部で1.5秒(設定しても最大60秒)しかありません。時間のかかる処理は、一覧に書くだけにして別の仕組みに任せます。

演習手を動かして確かめる

演習1

上の guard.py を入れる前に、止めたいコマンドと止めたくないコマンドを用意して試したいです。それぞれ2つずつ挙げてください。

答えを見る

止めたい例:plutil -extract Label raw ~/Library/LaunchAgents/x.plist(-o が無い)、git push --force origin main

止めたくない例:plutil -extract Label raw -o - x.plist(-o - がある)、git push origin main

試すときは、echo '{"tool_input":{"command":"…"}}' | ~/.claude/hooks/guard.py; echo $? のように入力を流して、終了コードが2と0になるかを見ます。止めたくない側が止まってしまうと、ふだんの作業が進まなくなるので、両方を必ず試します。

演習2

PreToolUse に、外部の API に問い合わせて「このコマンドは危ないか」を判定するフックを置きました。API の応答が遅い日に、危ないコマンドが通ってしまいました。なぜでしょうか。

答えを見る

フックが時間切れになり、時間切れの PreToolUse フックは道具の実行を止めないからです。止める役のフックに、遅くなりうる処理(外部への問い合わせ、重い計算)を入れるのは向いていません。決まった形で判定できる部分を手元のコードで先に止め、外部の判定は補助にとどめます。

自分は、読むだけのはずのコマンドが原本を上書きしていた事故で、設定ファイルを51件壊したことがあります。それ以来、危ない使い方は機械で止め、CLAUDE.md には理由の説明だけを書くようにしています。

用語メモ本文の ※番号の説明

  1. ※1フック(hooks) Claude Code の決まったタイミング(道具を使う前後、会話の始まりと終わりなど)に、決めたコマンドを自動で走らせる仕組み。AIの判断を通らないので、必ず効く。 本文へ戻る ↩
  2. ※2標準入力(stdin) プログラムがデータを受け取る入り口の1つ。フックのコマンドは、何が起きようとしているかを、ここから JSON で受け取る。 本文へ戻る ↩
  3. ※3標準エラー出力(stderr) プログラムがエラーや注意を書き出す出口。フックが終了コード2で止めたとき、ここに書いたメッセージがAIに渡る。 本文へ戻る ↩
  4. ※4fail-open 判断できないとき(時間切れ、エラー)に「通す」ほうに倒れる作り。反対の fail-closed は「止める」ほうに倒れる。止める役の仕組みが fail-open だと、肝心なときに止められない。 本文へ戻る ↩

出典確かめた資料(2026年10月時点)

読めたらチェック

この回:0 / 3