「任せておいたら、消してほしくないものまで消えていた」。そういう話を一度でも聞くと、手が止まります。

Claude Codeは、git pushもファイルの削除も人に代わって実行します。便利さと危うさは、同じ場所にあります。だから、危ない一手を実行の直前で止める場所が要ります。その入り口がPreToolUseフックです。

この記事は、仕組みとPythonの実装、テストの書き方、誤検知を減らす調整の考え方までを追います。根拠は公式ドキュメントと、運営元WEBMARKSの本番運用の実例です。読み終えたら、自分の環境にフックを置いて、効いているところまで確かめられます。

本記事の検証環境:claude-opus-5 / Claude Code v2.1.x / macOS 15 / 2026-07-28検証。ここに書く挙動は、この条件で動かして確かめたものです。

進行役は3人です。若葉さん(Web担当2年目)が用語のそもそもを聞き、高梨課長が自分の手で動かす側の疑問を出し、鈴木さん(本誌監修)が答えます。

こんなふうに調べていませんか

  • AIエージェントにコマンドを任せているが、消してはいけないものを消されないか落ち着かない
  • 「危ないコマンドは止まるようにしておいて」と言われたが、どこに何を書けばいいか分からない

この記事を読み終えたときに手に入るもの

  • 実行の直前に割り込むフックを、自分の環境に置けるようになります
  • allow・deny・ask・deferを、止まる側と進む側に分けて説明できるようになります
  • 誤検知で仕事が止まらないように、denyとaskを配分できるようになります

結論30秒でわかる、この記事の結論

  • PreToolUseフックは、ツールが実行される直前に割り込んで、可否をスクリプトに判定させる仕組みです
  • 返す値はallow・deny・ask・deferの4つ。止めるのがdeny、人に戻すのがaskです
  • フックだけを守りの最後尾にしません。権限ルールと重ねて置きます
危険コマンドの遮断は、この順で組み立てます書いて、登録して、止めない側まで確かめます危険コマンドの遮断は、この順で組み立てます手順1判定をスクリプトへツール名を絞るだけでは中身まで見られない手順2設定ファイルへ登録読み直しは自動。開いたままでも効き始める手順3誤検知まで検査普段打つコマンドが黙って通るかを見る鈴木さん書いて、登録して、止めない側まで確かめます
危険コマンドの遮断は、この順で組み立てます — 書いて、登録して、止めない側まで確かめます

01Claude CodeのPreToolUseで危険コマンドを遮断すると、AIエージェントの手はどこで止まるんですか?

若葉さん
若葉さんの発言

PreToolUseって、名前からすると「ツールを使う前」ですよね。具体的には、どこに割り込むんでしょうか。

鈴木さん
鈴木さんの発言

実行ボタンの手前です。ツールを呼ぼうとした瞬間に発火して、通してよいかをスクリプトに聞きにいきます。改札で切符を確かめるのに近いですね。通したあとで確かめるのでは遅い、という発想です。

PreToolUseフックとは、Claude Codeがツールを実行する直前に発火し、実行の可否をスクリプトで判定できる仕組みです。

判定は4つの値で返します。allow(許可)・deny(拒否)・ask(人に確認)・defer(保留)です(出典: Claude Code公式ドキュメント)。

deferだけは性格が違います。claude -pをサブプロセスとして動かす連携用の値で、ツール呼び出しを保留したまま終了します。対話セッションでは警告が記録され、フックの結果は無視されます(出典: 同)。手元で対話しながら使う場面で、deferを当てにするのは筋がよくありません。

4つを並べて覚えるより、行き先で畳んだほうが実務では早く動けます。

4つの値は、行き先が2つに分かれます覚える順番は、名前より行き先が先です4つの値は、行き先が2つに分かれます覚える順番は、名前より行き先が先です手が止まる側deny — その場で終わり、理由が返るask — 人の確認をはさんでから続くdefer — 非対話の連携で保留のまま終わるdeferは対話中の利用では当てにできません手が進む側allow — 確認なしでそのまま実行無言 — 通常の権限設定へ委ねる条件を書き忘れた操作は、こちらへ落ちます
4つの値は、行き先が2つに分かれます — 覚える順番は、名前より行き先が先です

止まる側にあるのはdenyとask、それにdeferです。進む側はallowと、そしてスクリプトが何も出力しなかった場合です。

ここが最初の分かれ目になります。スクリプトが黙っていれば、コマンドはそのまま実行されます。フックは「止める理由を見つけた一手」だけを拾う仕組みで、拾いそこねたものは通ります。

この章のまとめ

PreToolUseは実行の直前に割り込みます。返す値は4つですが、実務では止まる側と進む側の2つに畳んで考えると迷いません。

02AIエージェントにBashを任せる前に、Claude Codeのどこへ書き込む権限が要るんですか?

今回対象にするのはBashコマンドです。先に用意するものが2つあります。

1つ目は、設定ファイルへの書き込み権限です。.claude/settings.json(プロジェクト共有)か、~/.claude/settings.json(個人設定)のどちらかに書きます。

2つ目は、スクリプトを動かす実行体です。commandに指定したもの、たとえばpython3が動く環境が要ります。

設定ファイルは変更検知で自動再読込されるため、Claude Codeの再起動は要りません(出典: Claude Code公式)。書き換えたら、その場から効きます。

03チームのAI導入で全員に効かせるには、Claude Codeのどの階層へ置くんですか?

置き場所は、スコープの優先順位で決まります。

スコープ置き場所優先度
Managed組織の管理ポリシー最優先(上書き不可)
コマンドライン引数セッション起動時の指定2番目
Local.claude/settings.local.json3番目
Project.claude/settings.json4番目
User~/.claude/settings.json最下位

読み方は単純です。上にあるものほど強く、下にあるものは上書きされます。組織の管理ポリシーがいちばん強く、手元の個人設定はいちばん弱い、という並びです。

チームで効かせたい遮断を個人設定に書くと、その人の手元でしか効きません。逆に、試したいだけの条件をプロジェクト側に書くと、全員が巻き込まれます。どちらの事故も、この表を先に見ておけば避けられます。

04AIエージェントに渡さない危険コマンドの条件は、スクリプトのどこに書くんですか?

高梨課長
高梨課長の発言

設定に「rmは禁止」と書けば済む話ではないんですか。

鈴木さん
鈴木さんの発言

そこが分かれ目です。matcherが絞れるのはツール名だけなんですよ。「Bashを使おうとしたら呼んでくれ」までは言えますが、「rm -rfだったら」はmatcherの仕事ではありません。中身の判定はスクリプト側に置きます。

高梨課長
高梨課長の発言

受付と、持ち物を見る係が別、ということですね。

絞る係と、中身を見る係は別です受付を通ったことは、安全の証明にはなりません絞る係と、中身を見る係は別です受付を通ったことは、安全の証明にはなりません建物でいうとフックでいうとどの窓口へ行くかだけ見る受付matcher — ツール名だけを絞る荷物を開けて中身を確かめる係スクリプト — 文字列そのものを読む受付だけ置いて中を見ない建物matcherしか書いていない設定窓口を絞ることと、可否を決めることは別の仕事です。
絞る係と、中身を見る係は別です — 受付を通ったことは、安全の証明にはなりません

次のdanger-guard.pyは、標準入力のJSONからtool_input.commandを読み、危険なコマンドだけをdeny・askで返す最小構成です。

#!/usr/bin/env python3
"""danger-guard.py — Bash用PreToolUseフック。
実行前のコマンド文字列を判定し、危険なものだけdeny/askで返す。
該当なしのときは何も出力せず終了する(=通常の権限設定に任せる)。
"""
import json
import re
import sys

# 即座に止める操作。動詞と対象の組み合わせで見て、誤検知を抑える。
DENY_RULES = [
    (re.compile(r"\brm\s+-rf\s+/(?:\s|$)"), "ルート直下へのrm -rfは遮断します"),
    (re.compile(r"\bgit\s+push\b[^\n]*--force\b"), "force pushは人間の承認が必要です"),
    (re.compile(r"\bgit\s+reset\s+--hard\b"), "reset --hardは未コミットの変更を消します"),
]

# 誤検知が起きやすい操作。denyせず、人への確認に留める。
ASK_RULES = [
    (re.compile(r"\bfind\b[^\n]*-delete\b"), "find -deleteは対象範囲を確認してから実行してください"),
]


def decide(command: str):
    for pattern, reason in DENY_RULES:
        if pattern.search(command):
            return "deny", reason
    for pattern, reason in ASK_RULES:
        if pattern.search(command):
            return "ask", reason
    return None, None


def main() -> None:
    payload = json.load(sys.stdin)
    command = payload.get("tool_input", {}).get("command", "")
    if not isinstance(command, str) or not command:
        return
    decision, reason = decide(command)
    if decision is None:
        return
    print(json.dumps({
        "hookSpecificOutput": {
            "hookEventName": "PreToolUse",
            "permissionDecision": decision,
            "permissionDecisionReason": reason,
        }
    }))


if __name__ == "__main__":
    main()

読みどころは3つあります。

1つ目は、拒否する条件と確認に留める条件を、別の入れ物に分けている点です。取り返しがつかない操作と、範囲さえ分かれば進めてよい操作は、同じ棚に置きません

2つ目は、動詞と対象の組み合わせで見ている点です。rmという語だけを拾うと、無関係なコマンドまで巻き込みます。rm -rf /という形まで見て、はじめて止めます。

3つ目は、該当なしのときに何も出力しない点です。ルールに当たらなければ終了コード0のまま戻り、Claude Codeは通常どおり実行します。deny・askのときも、標準出力にJSONを書くだけで終了コードは0のままです。公式のサンプルスクリプトも同じ方式です(出典: Claude Code公式)。

05AIエージェントを動かしたまま、Claude Codeのsettings.jsonへフックを登録できますか?

できます。設定ファイルは変更検知で自動再読込されるため、セッションを開いたままでも書き換えが効きます(出典: Claude Code公式)。

スクリプトを置いたら、.claude/settings.jsonhooks.PreToolUseに登録します。

{
  "hooks": {
    "PreToolUse": [
      {
        "matcher": "Bash",
        "hooks": [
          {
            "type": "command",
            "command": "${CLAUDE_PROJECT_DIR}/.claude/hooks/danger-guard.py"
          }
        ]
      }
    ]
  }
}

matcherの書き方は3通りあります(出典: Claude Code公式)。Bashのような完全一致、Edit|Writeのようなパイプ区切りのリスト、mcp__.*のような正規表現です。

対象を広げるほど、スクリプトが呼ばれる場面は増えます。最初からEditやWriteまで広げると、どの条件が効いているのか読めなくなります。Bashだけに絞って動き方を見るところから始めると、原因の切り分けが楽になります。

06PreToolUseの判定は、AIエージェントに任せる前にどうテストするんですか?

高梨課長
高梨課長の発言

書いたスクリプトが正しいかは、どう確かめるんでしょう。本番で試すのは怖いです。

鈴木さん
鈴木さんの発言

登録する前に、単体で動かします。標準入力にJSONを流し込むだけなので、テストコードから呼べますよ。危険なコマンドを1件、安全なコマンドを1件。最低でもこの2種類は用意します。

高梨課長
高梨課長の発言

危険なほうだけ試して終わり、にしがちですね。

鈴木さん
鈴木さんの発言

そこが落とし穴です。止める性能より、止めない性能のほうが運用では効きます。

import json
import subprocess


def run_hook(command: str) -> subprocess.CompletedProcess:
    payload = {"tool_input": {"command": command}}
    return subprocess.run(
        ["python3", "danger-guard.py"],
        input=json.dumps(payload),
        text=True,
        capture_output=True,
    )


def test_blocks_force_push():
    result = run_hook("git push origin main --force")
    assert result.returncode == 0
    output = json.loads(result.stdout)
    assert output["hookSpecificOutput"]["permissionDecision"] == "deny"


def test_allows_normal_push():
    result = run_hook("git push origin main")
    assert result.returncode == 0
    assert result.stdout.strip() == ""

見落とされがちなのは、2つ目のテストのほうです。標準出力が空であることを確認しています。空でなければ、通常のコマンドまで誤ってdenyしている証拠になります

ダミーの危険コマンドを並べたテストは、通って当たり前です。日常的に打つ安全なコマンドも、同じ数だけテストに残してください。条件を1つ足すたびに、安全側のテストも1つ足す。この対で増やしていくと、あとから条件を緩めるときの根拠が残ります。

07PreToolUseの標準出力とexit codeを間違えると、AIエージェントは止まらずに進んでしまうんですか?

進んでしまいます。しかも、その場では気づけません。

スクリプトの標準出力には、JSON以外を書けません。シェルの起動プロファイルの出力が混ざると、解析に失敗します。デバッグ用にprintを1行足しただけで、判定が届かなくなります

そしてブロックできるのはexit code 2だけです。exit code 1は非ブロッキングのまま呼び出しが続きます(出典: Claude Code公式)。例外で落ちて意図せずexit 1になっていないかを、テストで確かめてください。

同じdenyでも、届く形と届かない形があります届かないほうは、画面上ではまったく静かです同じdenyでも、届く形と届かない形があります届かないほうは、画面上ではまったく静かです判定が届かない出力にJSON以外の行が混ざる起動プロファイルの文字が流れ込む例外で落ちて終了コードがずれる警告も出ないまま、呼び出しは続きます判定が届く出力はJSONだけに保つ遮断は終了コード2の経路で返すこの形になっているかを検査で押さえます
同じdenyでも、届く形と届かない形があります — 届かないほうは、画面上ではまったく静かです

「止めたつもりで止まっていない」状態は、画面の上では静かです。エラーも警告も出ないまま、コマンドが実行されます。動いているように見えるフックほど、先に踏んで確かめる価値があります。

08権限ルールとPreToolUseフックは、AI導入のときどちらから先に決めるんですか?

高梨課長
高梨課長の発言

permissionsのほうにもdenyがありますよね。フックと二重になりませんか。

鈴木さん
鈴木さんの発言

二重で構いません。というより、重ねるのが前提です。書き方も、判定できる範囲も違いますから。

観点権限ルール(permissionsPreToolUseフック
書き方Bash(rm -rf*)のようなパターン文字列Pythonなど任意言語のスクリプト
判定できる範囲コマンドのプレフィックス一致正規表現・外部ファイル参照など任意のロジック
設定場所settings.jsonpermissionssettings.jsonhooks.PreToolUse
向く用途既知の危険パターンを素早く止める状況に応じた判定、deny・askの使い分け

順番でいえば、権限ルールが先です。パターン文字列で書けるものを、わざわざスクリプトへ持ち込む理由はありません。書ける形で書いておけば、読む人も増えます。

2つは対立せず、重なりを持ちます違うのは思想ではなく、書ける範囲です2つは対立せず、重なりを持ちます違うのは思想ではなく、書ける範囲です権限ルールPreToolUseフックパターン文字列/設定ファイルに直接任意の言語/正規表現や外部参照既知の危ない形既知の危ない形 : どちらにも置ける運営元の本番設定は、この重なりを両方に持たせています。
2つは対立せず、重なりを持ちます — 違うのは思想ではなく、書ける範囲です

運営元WEBMARKSの本番設定は、両方を併用しています。permissions.denyにはBash(rm -rf*)Bash(git push --force*)のような既知パターンを置いています。PreToolUseフックには、機密ファイルへのアクセス遮断や難読化対策など、プレフィックス一致では書けない判定をまとめています(2026-07-28にリポジトリを直接確認)。

この章のまとめ

先に権限ルールで既知のパターンを塞ぎ、そこからこぼれた判定をフックへ回します。順番を逆にすると、スクリプトだけが太っていきます。

09危険コマンドの遮断でdenyを増やしすぎると、AI活用そのものが止まりませんか?

止まります。そしてこれは、実際によく起きるほうの失敗です。

境界線上のコマンドまでdenyにすると、正当な作業までブロックされます。作業が止まれば、人はフックを外しにかかります。外されたフックは、何も守りません。

確信度が低い判定はaskに落としてください。ユーザーが許可すれば、実行が続く余地が残ります(出典: Claude Code公式)。

守りは、土台から順に積みますこぼれたぶんを、上の層が受け止める形です守りは、土台から順に積みますこぼれたぶんを、上の層が受け止める形です③ 人への確認確信が持てない一手は、実行の手前で人へ返す② スクリプトによる拒否文字列では書けない判定だけを引き受ける① 既知パターンの遮断文字列で書けるものは、文字列のまま塞ぐ
守りは、土台から順に積みます — こぼれたぶんを、上の層が受け止める形です

配分の基準は、こう置くと決めやすくなります。取り返しがつかない操作かどうかがdenyの側、文脈しだいで正解が変わる操作かどうかがaskの側です。迷ったらask、という置き方でも構いません。

10動詞と対象の組み合わせで遮断を判定する方式は、AIエージェントの想定外にどこまで届くんですか?

運営元WEBMARKSの本番フックも、動詞と対象の組み合わせで判定する方式です。

この方式は誤検知を減らせます。一方で、組み合わせの網羅は設計者の想定に依存します。想定していない組み合わせは、そのまま通ります

だから、この方式だけを最終防衛線にはしていません。権限ルールと重ねて運用しています。届く範囲が有限だと分かっているものを、単独で最後尾に置かない、という考え方です。

2026-07-28時点で、1本のスクリプトに20種類超の拒否条件が積み重なっています。対応するテストファイルで検証しています。条件は事故のたびに増えていくもので、増えること自体は健全です

11Claude CodeのPreToolUseが危険コマンドを遮断したことは、AIエージェントの画面のどこで分かるんですか?

若葉さん
若葉さんの発言

登録したあと、ちゃんと効いているかはどこを見れば分かりますか。

鈴木さん
鈴木さんの発言

見る場所は2つあります。/hooksと、デバッグ起動のログですね。そのうえで、実際に危ないコマンドを打ってみるのがいちばん確かです。

/hooksと入力すると、設定済みフックを読み取り専用で一覧できます(出典: Claude Code公式)。個別の実行内容は、--debug起動時のログに残ります。

本記事の執筆中にも、保護対象のパスを含むコマンドを打った瞬間にPreToolUseフックが発火し、その場でdenyされました(2026-07-28実測)。返ってきた拒否理由はそのままエラーとして表示され、コマンドは実行されていません。

効いていると言えるのは、この状態ですそろって初めて、遮断できたと言えます効いていると言えるのは、この状態ですそろって初めて、遮断できたと言えます止める側と止めない側、どちらの検査もグリーンである一覧画面に、スクリプトの所在と対象ツールが並んでいる危ない操作を打つと、返された理由がそのまま読める普段の操作では、どちらの出力にも文字が出てこないどれか欠けたまま、本番で使い始めてしまう止まったように見えて止まっていない状態が残ります
効いていると言えるのは、この状態です — そろって初めて、遮断できたと言えます

この項目は、どれか1つでも欠けると意味が変わります。テストは通っているのに登録できていない、登録はできているのに拒否理由が読めない、といった中途半端な状態が残るからです。

拒否理由が画面に出るかどうかは、とくに見落とされます。理由が読めないdenyは、使う人にとっては原因不明の停止と同じで、いずれ外されます

この章のまとめ

遮断できたかどうかは、画面とログの両方で確かめます。テストが通ったことは、登録できたことの証拠にはなりません。

12よくある質問

denyにしたコマンドを、Claudeは別の書き方で試せますか

denyは最終判定で、Claudeは同じ呼び出しを再試行できません(出典: Claude Code公式)。ただし、別の書き方で渡し直せば、その文字列を改めて判定します。意味は同じでも表記が違うコマンドは、条件を書いていなければ通ります。動詞と対象の組み合わせで判定する方式が効いてくるのは、まさにここです。

個人設定と共有設定、どちらにフックを書くべきですか

チーム全員に効かせたい危険コマンドの遮断は、Project(.claude/settings.json)に置いてGitで共有します。個人の一時的な上書きだけをLocal(.claude/settings.local.json)に置きます。優先順位は前提の章の表のとおりです(出典: Claude Code公式)。組織の管理ポリシー(Managed)はいちばん強く、手元の設定では上書きできません。

フックを登録したら、Claude Codeを再起動する必要がありますか

要りません。設定ファイルは変更検知で自動再読込されます(出典: Claude Code公式)。セッションを開いたまま書き換えても効きます。反映されていないように見えるときは、再起動より先に、JSONの書式とcommandに書いたパスを疑ってください。標準出力に余計な文字が混ざっていないかも、あわせて確かめます。

安全なコマンドまで止まってしまったら、どう直せばいいですか

まず、どの条件に当たったかをpermissionDecisionReasonで確かめます。理由がそのまま画面に出るので、当たったルールを特定できます。次に、その条件をdenyからaskへ落とすか、動詞と対象の組み合わせをもう一段細かくします。条件そのものを消すのは最後の手段です。消せば、止めたかった操作まで通るようになります。

13まとめ|今日やる3つのこと

止めたい一手は、実行される前にしか止められません。フックはそのための場所で、書く順番さえ間違えなければ短時間で置けます。

今日はこの順で手をつけます

  1. 権限ルールで既知の危険パターンを先に塞ぐ

    パターン文字列で書けるものを、スクリプトに持ち込まないためです

  2. danger-guard.pyを置き、危険なコマンドと安全なコマンドを同じ数だけテストする

    止めない性能を先に確かめます

  3. /hooksで登録を確認し、実際に危ないコマンドを打ってみる

    テストの合格は、登録できた証拠にはなりません

AI検索では、こう聞かれています

  • Claude CodeのPreToolUseフックって、何をする仕組みなんですか?

    「Claude CodeのPreToolUseで危険コマンドを遮断すると、AIエージェントの手はどこで止まるんですか?」の章で説明しています

  • 危険なコマンドを止めるフックは、どう書けばいいんですか?

    「AIエージェントに渡さない危険コマンドの条件は、スクリプトのどこに書くんですか?」の章にコードがあります

  • PreToolUseと権限設定は、どちらを使えばいいんですか?

    「権限ルールとPreToolUseフックは、AI導入のときどちらから先に決めるんですか?」の章で使い分けを扱っています

  • フックが効いているかは、どこで確かめられるんですか?

    「Claude CodeのPreToolUseが危険コマンドを遮断したことは、AIエージェントの画面のどこで分かるんですか?」の章にあります

次に読むなら、この記事です