
Claude Code Hooks の if フィルタで特定コマンドだけをフックする
どうも!オペ部の西村祐二です!
Claude Codeの v2.1.85 で、Hooks に if 条件フィルタが追加されました。フックの発火条件をコマンド引数レベルで絞り込める機能です。
Hooks は権限モードに依存せず動作するため、--dangerously-skip-permissions でも if 条件付きフックでディレクトリ削除や git push をブロックできます。この記事では if フィルタの使い方と、権限バイパスモードでのガードレールとしての活用方法を紹介します。
if 条件フィルタとは
Hooks の各ハンドラーに if フィールドが追加されました。permission rule syntax を使って、フックの発火条件をツール引数のレベルまで絞り込めます。
従来の matcher はツール名(Bash、Edit など)でしかフィルタできませんでした。if を使うと「rm で始まるコマンドだけ」「git コマンドだけ」のように、引数の内容まで条件指定できます。
条件にマッチしない場合はフックのプロセス自体が起動しないため、パフォーマンスの面でもメリットがあると考えられます。
v2.1.85 時点では複合コマンド(ls && git push)や環境変数プレフィックス付きコマンド(FOO=bar git push)にマッチしないバグがありましたが、v2.1.89 で修正され、サブコマンド単位でマッチするようになっています。
Hooks は権限モードに依存しない
--dangerously-skip-permissions(bypassPermissions モード)を使うと、ツール実行時の承認ダイアログがスキップされます。CI/CD やバッチ処理など無人で実行するケースでは便利ですが、rm -rf や git push --force のような破壊的なコマンドもそのまま実行されてしまいます。
ここで重要なのは、Hooks は権限モードとは独立して動作するという点です。Hooks Guide の「Hooks and permission modes」セクションには、以下の記載があります。
PreToolUse hooks fire before any permission-mode check. A hook that returns
permissionDecision: "deny"blocks the tool even inbypassPermissionsmode or with--dangerously-skip-permissions. This lets you enforce policy that users cannot bypass by changing their permission mode.
つまり、--dangerously-skip-permissions で承認ダイアログをスキップしつつ、Hooks で特定の危険な操作だけをブロックするという運用が可能です。if 条件フィルタと組み合わせることで、ブロック対象を引数レベルで絞り込めるようになりました。
if 以前の課題
v2.1.85 以前は、特定のコマンドだけをフックしたい場合でも、matcher: "Bash" で全 Bash コマンドを対象にした上で、スクリプト内で判定する必要がありました。
{
"hooks": {
"PreToolUse": [
{
"matcher": "Bash",
"hooks": [
{
"type": "command",
"command": ".claude/hooks/pre-bash-firewall.sh"
}
]
}
]
}
}
#!/bin/bash
input=$(cat)
command=$(echo "$input" | jq -r '.tool_input.command // empty')
# スクリプト内で対象コマンドを判定
if [[ "$command" == rm\ * ]] || [[ "$command" == git\ reset\ --hard* ]]; then
echo "Blocked: $command" >&2
exit 2
fi
この方式には2つの課題がありました。
- 全 Bash コマンドでプロセスが起動する:
lsやcatのような安全なコマンドでもフックスクリプトのプロセスが spawn されます。頻繁にツールを実行するエージェントでは、このオーバーヘッドが積み重なります - 判定ロジックがスクリプトに分散する: 何をブロックしているかが設定ファイルからは分からず、スクリプトを読まないと把握できません
if フィールドを使うと、これらの課題が解消されます。
{
"hooks": {
"PreToolUse": [
{
"matcher": "Bash",
"hooks": [
{
"type": "command",
"if": "Bash(rm *)",
"command": "bash ~/.claude/hooks/block-rm.sh"
},
{
"type": "command",
"if": "Bash(git reset --hard*)",
"command": "bash ~/.claude/hooks/block-git-reset.sh"
}
]
}
]
}
}
条件にマッチしないコマンドではプロセスが起動せず、何をブロックしているかも設定ファイルから一目で分かります。
試してみる
環境
- Claude Code 2.1.89
- macOS
前提条件
jqコマンドがインストール済みであること(brew install jqなどで導入できる)
1. rm と git push を共通スクリプトでブロックする
rm コマンドと git push を、--dangerously-skip-permissions のときだけブロックする例です。
.claude/settings.json に以下の設定を追加します。
{
"hooks": {
"PreToolUse": [
{
"matcher": "Bash",
"hooks": [
{
"type": "command",
"if": "Bash(rm *)",
"command": "bash ~/.claude/hooks/block-bypass.sh"
},
{
"type": "command",
"if": "Bash(git push*)",
"command": "bash ~/.claude/hooks/block-bypass.sh"
}
]
}
]
}
}
ポイントは以下の2点です。
"if"でコマンドごとに発火条件を絞り込んでいるため、スクリプト側でコマンドの判定は不要。同じスクリプトを複数のifで共有できるmatcherがBashで全 Bash コマンドを対象にしつつ、ifでrm *とgit push*だけに絞り込んでいる
フックスクリプトは以下のようになります。
#!/bin/bash
input=$(cat)
command=$(echo "$input" | jq -r '.tool_input.command // empty')
permission_mode=$(echo "$input" | jq -r '.permission_mode // empty')
# bypassPermissions モード(--dangerously-skip-permissions)のときだけブロック
if [[ "$permission_mode" == "bypassPermissions" ]]; then
echo "[block-bypass] Blocked in bypassPermissions mode: $command" >&2
exit 2
fi
フックスクリプトが終了コード 2 を返すと、ツールの実行がブロックされます。stderr に出力したメッセージはモデルにフィードバックされます。

permission_mode フィールドで現在の権限モードを判定しています。--dangerously-skip-permissions で実行している場合は bypassPermissions が入るため、このモードのときだけブロックし、通常モードでは Claude の標準の権限確認に任せるという動作になります。
if でフィルタしているおかげで、ls や cat などのコマンドではフックプロセス自体が起動しません。また if: "Bash(git push*)" は git push、git push origin main、git push --force のいずれにもマッチし、ls && git push のような複合コマンドや GIT_SSH_COMMAND="ssh -i key" git push のような環境変数プレフィックス付きコマンドにも正しくマッチします。
フックの入出力を活用する
ここまでの例では exit 2 でシンプルにブロックしていました。実際には、フックスクリプトは stdin で詳細なコンテキスト情報を受け取り、stdout で JSON を返すことでより細かい制御が可能です。
PreToolUse の入力 JSON
PreToolUse フックのスクリプトは stdin で以下の JSON を受け取ります。
{
"session_id": "セッション識別子",
"transcript_path": "会話ログのファイルパス",
"cwd": "現在の作業ディレクトリ",
"permission_mode": "bypassPermissions",
"hook_event_name": "PreToolUse",
"tool_name": "Bash",
"tool_input": {
"command": "rm -rf /tmp/test",
"description": "Remove test directory"
},
"tool_use_id": "ツール呼び出しの識別子"
}
| フィールド | 説明 |
|---|---|
session_id |
セッションの識別子。監査ログでセッション単位の追跡に使える |
permission_mode |
現在の権限モード。default、bypassPermissions、auto、plan など |
tool_name |
ツール名。Bash、Edit、Write、Read など |
tool_input |
ツールへの入力。Bash の場合は command フィールドにコマンド文字列が入る |
tool_use_id |
ツール呼び出しごとの識別子 |
cwd |
現在の作業ディレクトリ |
transcript_path |
会話ログの JSON ファイルパス |
PreToolUse の出力 JSON
stdout に JSON を返すことで、exit 2 よりも細かい制御ができます。
{
"hookSpecificOutput": {
"hookEventName": "PreToolUse",
"permissionDecision": "deny",
"permissionDecisionReason": "ブロック理由",
"additionalContext": "モデルへの追加コンテキスト"
}
}
| フィールド | 説明 |
|---|---|
permissionDecision |
deny でブロック、allow で許可、ask でユーザーに確認、defer で他のフックに委譲 |
permissionDecisionReason |
ブロック理由。ログにも表示される |
additionalContext |
ブロック後にモデルへ渡される追加コンテキスト。代替手段を促すメッセージなどに使える |
3. 入力フィールドと additionalContext を活用する
bypassPermissions モードのとき permissionDecision: "ask" でユーザーに確認を求める例です。「試してみる」の例 1 をベースに拡張しています。
{
"hooks": {
"PreToolUse": [
{
"matcher": "Bash",
"hooks": [
{
"type": "command",
"if": "Bash(rm *)",
"command": "bash ~/.claude/hooks/check-rm.sh",
"statusMessage": "Checking for dangerous rm command..."
}
]
}
]
}
}
設定側では2つのプロパティを追加しています。
"statusMessage": フック実行中にスピナーに表示されるメッセージ
#!/bin/bash
input=$(cat)
# フック入力 JSON から各フィールドを取得
command=$(echo "$input" | jq -r '.tool_input.command // empty')
tool_name=$(echo "$input" | jq -r '.tool_name // empty')
permission_mode=$(echo "$input" | jq -r '.permission_mode // empty')
session_id=$(echo "$input" | jq -r '.session_id // empty')
echo "[check-rm] mode=$permission_mode tool=$tool_name command=$command session=$session_id" >&2
# bypassPermissions モードのときだけユーザーに確認を求める
if [[ "$permission_mode" == "bypassPermissions" ]]; then
jq -n \
--arg reason "rm command detected in bypassPermissions mode: $command" \
--arg context "rm コマンドが検出されました。実行を許可するかユーザーに確認しています。" \
'{
hookSpecificOutput: {
hookEventName: "PreToolUse",
permissionDecision: "ask",
permissionDecisionReason: $reason,
additionalContext: $context
}
}'
fi
exit 2 のシンプルなブロックとの違いは以下の点です。
permissionDecision: "ask"でユーザーに確認ダイアログを表示する。--dangerously-skip-permissionsは承認ダイアログをスキップするが、フックからaskを返すことで確認を強制できるpermission_modeでbypassPermissionsのときだけ発動し、通常モードでは Claude の標準の権限確認に任せるadditionalContextでモデルに状況を伝え、permissionDecisionReasonにコマンド内容を含めることで確認ダイアログの理由が具体的になる

4. async で git 操作を監査ログに記録する
git コマンドの実行を非同期で監査ログに記録する例です。
{
"hooks": {
"PostToolUse": [
{
"matcher": "Bash",
"hooks": [
{
"type": "command",
"if": "Bash(git *)",
"command": "bash ~/.claude/hooks/log-git.sh",
"async": true,
"timeout": 5
}
]
}
]
}
}
#!/bin/bash
input=$(cat)
command=$(echo "$input" | jq -r '.tool_input.command // empty')
permission_mode=$(echo "$input" | jq -r '.permission_mode // empty')
session_id=$(echo "$input" | jq -r '.session_id // empty')
cwd=$(echo "$input" | jq -r '.cwd // empty')
tool_use_id=$(echo "$input" | jq -r '.tool_use_id // empty')
timestamp=$(date '+%Y-%m-%d %H:%M:%S')
log_dir="$(git rev-parse --show-toplevel)/.claude"
mkdir -p "$log_dir"
# TSV 形式で監査ログに記録
echo -e "$timestamp\t$session_id\t$permission_mode\t$cwd\t$command\t$tool_use_id" \
>> "$log_dir/git-audit.log"
session_id や permission_mode、cwd、tool_use_id を含めることで、どのセッションのどの権限モードでどのコマンドが実行されたかを追跡できます。
"async": true: フックをバックグラウンドで実行し、完了を待たずにツールの実行に進みます。ログ記録のようにブロックする必要がない処理に適しています"timeout": 5: 非同期でもタイムアウトは有効です。ログ書き込みが遅延した場合に備えて短く設定しています
async フックでは stdout の出力(permissionDecision 等)は無視されます。ツールの実行をブロックしたい場合は async を使わず同期実行にする必要があります。
5. type: "prompt" で AI にコマンドを判定させる
フックの type には command(シェルスクリプト)以外に prompt(AI モデルによる判定)も指定できます。
{
"hooks": {
"PreToolUse": [
{
"matcher": "Bash",
"hooks": [
{
"type": "prompt",
"if": "Bash(git *)",
"prompt": "以下の git コマンドがリポジトリに破壊的な変更を加えるかどうかを判定してください。\n\nコマンド: $ARGUMENTS\n\n破壊的な変更とは、git push --force、git reset --hard、git clean -f、ブランチの削除などを指します。\n\n破壊的な場合は permissionDecision を deny に、そうでなければ allow にしてください。",
"timeout": 15
}
]
}
]
}
}
"type": "prompt": シェルスクリプトの代わりに、AI モデルがプロンプトに基づいてフックの判定を行います"$ARGUMENTS": フックに渡されるツール情報の JSON がこのプレースホルダーに展開されます"timeout": 15: prompt タイプのデフォルトタイムアウトは30秒ですが、判定は短時間で完了するため短めに設定しています

type: "prompt" は柔軟な判定ができる反面、AI モデルの推論を挟むため type: "command" よりも実行に時間がかかります。確実にブロックしたいコマンドは type: "command" で即座にブロックし、判断が必要なグレーゾーンのコマンドに type: "prompt" を使うという使い分けが考えられます。
if の構文まとめ
if フィールドは permission rule syntax を使います。主要なパターンは以下の通りです。
| 構文 | マッチ対象 | 用途 |
|---|---|---|
Bash(rm *) |
rm で始まるコマンド |
危険コマンドのブロック |
Bash(git *) |
git で始まるコマンド |
git 操作の監査ログ |
Bash(docker *) |
docker で始まるコマンド |
コンテナ操作の制御 |
Bash(npm publish*) |
npm publish で始まるコマンド |
パッケージ公開の制御 |
Edit(*.ts) |
TypeScript ファイルの編集 | 特定ファイルタイプの監視 |
Edit(src/**) |
src/ 配下のファイル編集(再帰) | ディレクトリ単位の制御 |
Edit(*.env*) |
.env ファイルの編集 | 機密ファイルの保護 |
まとめ
Claude Code v2.1.85 で追加された Hooks の if 条件フィルタを紹介しました。
- if 条件フィルタ:
matcherのツール名フィルタに加えて、引数レベルで発火条件を細かく指定できるようになった。不要なプロセス生成が減り、パフォーマンス面でもメリットがある - 権限モードとの独立性: Hooks は
--dangerously-skip-permissionsや auto モードでも動作する。承認ダイアログをスキップしつつ、特定の危険操作だけをブロックするガードレールとして使える
Hooks 自体は v1.0.38 で導入された機能ですが、if 条件フィルタの追加によって実用的な設定がしやすくなった印象があります。特に autoや--dangerously-skip-permissions で無人実行する際に、ディレクトリ削除や git push だけをピンポイントでブロックできるのは実用的と感じました。timeout や async、additionalContext などのプロパティを組み合わせることで、ガードレールとしてだけでなく監査ログや AI による柔軟な判定にも活用できそうです。
誰かの参考になれば幸いです。
関連リンク:







