Claude Codeはgit pushやファイル削除も人に代わって実行します。危険な一手を実行前に止める入り口が、PreToolUseフックです。本記事は仕組みとPythonコード、テストの書き方、誤検知を減らす調整の考え方を、公式ドキュメントと自社実装の実例で示します。
01Claude CodeのPreToolUseフックで危険コマンドを遮断する|できるようになること
判定結果は allow(許可)・deny(拒否)・ask(人に確認)・defer(通常の権限設定に任せる)の4種類で返します。
02前提:PreToolUseを動かすための環境とアクセス権限
今回はBashコマンドが対象です。.claude/settings.json(プロジェクト共有)か~/.claude/settings.json(個人設定)への書き込み権限が要ります。
スクリプトを動かすpython3など、commandの実行体が使える環境も必要です。設定ファイルは変更検知で自動再読込されるため、Claude Codeの再起動は不要です(出典: Claude Code公式)。
| スコープ | 置き場所 | 優先度 |
|---|---|---|
| Managed | 組織の管理ポリシー | 最優先(上書き不可) |
| コマンドライン引数 | セッション起動時の指定 | 2番目 |
| Local | .claude/settings.local.json | 3番目 |
| Project | .claude/settings.json | 4番目 |
| User | ~/.claude/settings.json | 最下位 |
検証環境はclaude-opus-5・Claude Code v2.1.x系・macOS 15、検証日は2026-07-28です。
03危険コマンドの条件をPreToolUseスクリプトに書く
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()該当ルールが無ければ何も出力せず終了コード0のまま戻り、Claude Codeは通常どおり実行します。deny・askも標準出力にJSONを書くだけで終了コードは0のままです。公式サンプルスクリプトも同じ方式です(出典: Claude Code公式)。
04settings.jsonへ実行前フックを登録する手順
スクリプトを置いたら、.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__.*は正規表現です。
05PreToolUseフックの判定をテストコードで確認する
スクリプトをsettings.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している証拠です。ダミーの危険コマンドだけでなく、日常的に打つ安全なコマンドも同じ数だけテストに残します。
06誤検知を減らすときに、PreToolUseの調整で見るべき観点
標準出力とexit codeの扱いを間違える
スクリプトの標準出力にはJSON以外を書けません。シェルの起動プロファイルの出力が混ざると解析に失敗します。ブロックできるのはexit code 2だけです。exit code 1は非ブロッキングのまま呼び出しが続くため、例外で落ちて意図せずexit 1になっていないかをテストで確かめます(出典: Claude Code公式)。
権限ルールとPreToolUseフックを混同する
| 観点 | 権限ルール(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にリポジトリを直接確認)。
全部denyにすると、誤検知で仕事が止まる
境界線上のコマンドまでdenyにすると正当な作業までブロックされます。確信度が低い判定はaskに落とし、ユーザーが許可すれば実行が続く余地を残します(出典: Claude Code公式)。
WEBMARKSの本番フックも、動詞と対象の組み合わせで判定する方式です。この方式は誤検知を減らせる反面、組み合わせの網羅は設計者の想定に依存し、想定外の組み合わせは通ってしまいます。この方式だけを最終防衛線にせず、権限ルールと重ねて運用しています。2026-07-28時点で1本のスクリプトに20種類超の拒否条件が積み重なり、対応するテストファイルで検証しています。
07動作確認の方法|PreToolUseが危険コマンドを遮断したかの判定基準
動作確認は/hooksとデバッグ起動で行います。/hooksと入力すると設定済みフックを読み取り専用で一覧でき(出典: Claude Code公式)、個別の実行内容は--debug起動時のログに残ります。
本記事の執筆中にも、保護対象のパスを含むコマンドを打った瞬間にPreToolUseフックが発火し、その場でdenyされました(2026-07-28実測)。返ってきた拒否理由はそのままエラーとして表示され、コマンドは実行されていません。
判定基準は次の4つです。
- deny用・allow用のテストコードが両方グリーンである
/hooksにスクリプトのパスとmatcherが表示されている- 実際に危険なコマンドを打ち、
permissionDecisionReasonの内容がそのまま画面に出る - 安全なコマンドを打ち、標準出力・stderrともに何も出ない
08FAQ
denyにしたコマンドを、Claudeは別の書き方で試せますか
denyは最終判定で、Claudeは同じ呼び出しを再試行できません(出典: Claude Code公式)。別の書き方で渡し直せば、その文字列を改めて判定します。
個人設定と共有設定、どちらにフックを書くべきですか
チーム全員に効かせたい危険コマンド遮断はProject(.claude/settings.json)でGit共有します。個人の一時的な上書きだけLocal(.claude/settings.local.json)に置きます。優先順位は前提の章の表のとおりです(出典: Claude Code公式)。