「任せておいたら、消してほしくないものまで消えていた」。そういう話を一度でも聞くと、手が止まります。
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です
- フックだけを守りの最後尾にしません。権限ルールと重ねて置きます
01Claude CodeのPreToolUseで危険コマンドを遮断すると、AIエージェントの手はどこで止まるんですか?
若葉さんPreToolUseって、名前からすると「ツールを使う前」ですよね。具体的には、どこに割り込むんでしょうか。
鈴木さん実行ボタンの手前です。ツールを呼ぼうとした瞬間に発火して、通してよいかをスクリプトに聞きにいきます。改札で切符を確かめるのに近いですね。通したあとで確かめるのでは遅い、という発想です。
PreToolUseフックとは、Claude Codeがツールを実行する直前に発火し、実行の可否をスクリプトで判定できる仕組みです。
判定は4つの値で返します。allow(許可)・deny(拒否)・ask(人に確認)・defer(保留)です(出典: Claude Code公式ドキュメント)。
deferだけは性格が違います。claude -pをサブプロセスとして動かす連携用の値で、ツール呼び出しを保留したまま終了します。対話セッションでは警告が記録され、フックの結果は無視されます(出典: 同)。手元で対話しながら使う場面で、deferを当てにするのは筋がよくありません。
4つを並べて覚えるより、行き先で畳んだほうが実務では早く動けます。
止まる側にあるのは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.json | 3番目 |
| Project | .claude/settings.json | 4番目 |
| User | ~/.claude/settings.json | 最下位 |
読み方は単純です。上にあるものほど強く、下にあるものは上書きされます。組織の管理ポリシーがいちばん強く、手元の個人設定はいちばん弱い、という並びです。
チームで効かせたい遮断を個人設定に書くと、その人の手元でしか効きません。逆に、試したいだけの条件をプロジェクト側に書くと、全員が巻き込まれます。どちらの事故も、この表を先に見ておけば避けられます。
04AIエージェントに渡さない危険コマンドの条件は、スクリプトのどこに書くんですか?
高梨課長設定に「rmは禁止」と書けば済む話ではないんですか。
鈴木さんそこが分かれ目です。matcherが絞れるのはツール名だけなんですよ。「Bashを使おうとしたら呼んでくれ」までは言えますが、「rm -rfだったら」は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.jsonのhooks.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になっていないかを、テストで確かめてください。
「止めたつもりで止まっていない」状態は、画面の上では静かです。エラーも警告も出ないまま、コマンドが実行されます。動いているように見えるフックほど、先に踏んで確かめる価値があります。
08権限ルールとPreToolUseフックは、AI導入のときどちらから先に決めるんですか?
高梨課長permissionsのほうにもdenyがありますよね。フックと二重になりませんか。
鈴木さん二重で構いません。というより、重ねるのが前提です。書き方も、判定できる範囲も違いますから。
| 観点 | 権限ルール(permissions) | PreToolUseフック |
|---|---|---|
| 書き方 | Bash(rm -rf*)のようなパターン文字列 | Pythonなど任意言語のスクリプト |
| 判定できる範囲 | コマンドのプレフィックス一致 | 正規表現・外部ファイル参照など任意のロジック |
| 設定場所 | settings.jsonのpermissions | settings.jsonのhooks.PreToolUse |
| 向く用途 | 既知の危険パターンを素早く止める | 状況に応じた判定、deny・askの使い分け |
順番でいえば、権限ルールが先です。パターン文字列で書けるものを、わざわざスクリプトへ持ち込む理由はありません。書ける形で書いておけば、読む人も増えます。
運営元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つのこと
止めたい一手は、実行される前にしか止められません。フックはそのための場所で、書く順番さえ間違えなければ短時間で置けます。
今日はこの順で手をつけます
権限ルールで既知の危険パターンを先に塞ぐ
パターン文字列で書けるものを、スクリプトに持ち込まないためです
danger-guard.pyを置き、危険なコマンドと安全なコマンドを同じ数だけテストする
止めない性能を先に確かめます
/hooksで登録を確認し、実際に危ないコマンドを打ってみるテストの合格は、登録できた証拠にはなりません
AI検索では、こう聞かれています
Claude CodeのPreToolUseフックって、何をする仕組みなんですか?
「Claude CodeのPreToolUseで危険コマンドを遮断すると、AIエージェントの手はどこで止まるんですか?」の章で説明しています
危険なコマンドを止めるフックは、どう書けばいいんですか?
「AIエージェントに渡さない危険コマンドの条件は、スクリプトのどこに書くんですか?」の章にコードがあります
PreToolUseと権限設定は、どちらを使えばいいんですか?
「権限ルールとPreToolUseフックは、AI導入のときどちらから先に決めるんですか?」の章で使い分けを扱っています
フックが効いているかは、どこで確かめられるんですか?
「Claude CodeのPreToolUseが危険コマンドを遮断したことは、AIエージェントの画面のどこで分かるんですか?」の章にあります
次に読むなら、この記事です