Claude Code Hooks の if フィルタで特定コマンドだけをフックする

Claude Code Hooks の if フィルタで特定コマンドだけをフックする

2026.04.02

どうも!オペ部の西村祐二です!

Claude Codeの v2.1.85 で、Hooksif 条件フィルタが追加されました。フックの発火条件をコマンド引数レベルで絞り込める機能です。

Hooks は権限モードに依存せず動作するため、--dangerously-skip-permissions でも if 条件付きフックでディレクトリ削除や git push をブロックできます。この記事では if フィルタの使い方と、権限バイパスモードでのガードレールとしての活用方法を紹介します。

if 条件フィルタとは

Hooks の各ハンドラーに if フィールドが追加されました。permission rule syntax を使って、フックの発火条件をツール引数のレベルまで絞り込めます。

従来の matcher はツール名(BashEdit など)でしかフィルタできませんでした。if を使うと「rm で始まるコマンドだけ」「git コマンドだけ」のように、引数の内容まで条件指定できます。

条件にマッチしない場合はフックのプロセス自体が起動しないため、パフォーマンスの面でもメリットがあると考えられます。

v2.1.85 時点では複合コマンド(ls && git push)や環境変数プレフィックス付きコマンド(FOO=bar git push)にマッチしないバグがありましたが、v2.1.89 で修正され、サブコマンド単位でマッチするようになっています。

Hooks は権限モードに依存しない

--dangerously-skip-permissionsbypassPermissions モード)を使うと、ツール実行時の承認ダイアログがスキップされます。CI/CD やバッチ処理など無人で実行するケースでは便利ですが、rm -rfgit 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 in bypassPermissions mode 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 コマンドを対象にした上で、スクリプト内で判定する必要がありました。

.claude/settings.json(従来の方法)
{
  "hooks": {
    "PreToolUse": [
      {
        "matcher": "Bash",
        "hooks": [
          {
            "type": "command",
            "command": ".claude/hooks/pre-bash-firewall.sh"
          }
        ]
      }
    ]
  }
}
.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つの課題がありました。

  1. 全 Bash コマンドでプロセスが起動する: lscat のような安全なコマンドでもフックスクリプトのプロセスが spawn されます。頻繁にツールを実行するエージェントでは、このオーバーヘッドが積み重なります
  2. 判定ロジックがスクリプトに分散する: 何をブロックしているかが設定ファイルからは分からず、スクリプトを読まないと把握できません

if フィールドを使うと、これらの課題が解消されます。

.claude/settings.json(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 に以下の設定を追加します。

.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 で共有できる
  • matcherBash で全 Bash コマンドを対象にしつつ、ifrm *git push* だけに絞り込んでいる

フックスクリプトは以下のようになります。

.claude/hooks/block-bypass.sh
#!/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 に出力したメッセージはモデルにフィードバックされます。

CleanShot 2026-04-02 at 02.40.50@2x

permission_mode フィールドで現在の権限モードを判定しています。--dangerously-skip-permissions で実行している場合は bypassPermissions が入るため、このモードのときだけブロックし、通常モードでは Claude の標準の権限確認に任せるという動作になります。

if でフィルタしているおかげで、lscat などのコマンドではフックプロセス自体が起動しません。また if: "Bash(git push*)"git pushgit push origin maingit 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 現在の権限モード。defaultbypassPermissionsautoplan など
tool_name ツール名。BashEditWriteRead など
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 をベースに拡張しています。

.claude/settings.json
{
  "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": フック実行中にスピナーに表示されるメッセージ
.claude/hooks/check-rm.sh
#!/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_modebypassPermissions のときだけ発動し、通常モードでは Claude の標準の権限確認に任せる
  • additionalContext でモデルに状況を伝え、permissionDecisionReason にコマンド内容を含めることで確認ダイアログの理由が具体的になる

CleanShot 2026-04-02 at 11.05.28@2x

4. async で git 操作を監査ログに記録する

git コマンドの実行を非同期で監査ログに記録する例です。

.claude/settings.json
{
  "hooks": {
    "PostToolUse": [
      {
        "matcher": "Bash",
        "hooks": [
          {
            "type": "command",
            "if": "Bash(git *)",
            "command": "bash ~/.claude/hooks/log-git.sh",
            "async": true,
            "timeout": 5
          }
        ]
      }
    ]
  }
}
.claude/hooks/log-git.sh
#!/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_idpermission_modecwdtool_use_id を含めることで、どのセッションのどの権限モードでどのコマンドが実行されたかを追跡できます。

  • "async": true: フックをバックグラウンドで実行し、完了を待たずにツールの実行に進みます。ログ記録のようにブロックする必要がない処理に適しています
  • "timeout": 5: 非同期でもタイムアウトは有効です。ログ書き込みが遅延した場合に備えて短く設定しています

async フックでは stdout の出力(permissionDecision 等)は無視されます。ツールの実行をブロックしたい場合は async を使わず同期実行にする必要があります。

5. type: "prompt" で AI にコマンドを判定させる

フックの type には command(シェルスクリプト)以外に prompt(AI モデルによる判定)も指定できます。

.claude/settings.json
{
  "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秒ですが、判定は短時間で完了するため短めに設定しています

CleanShot 2026-04-02 at 11.40.23@2x

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 だけをピンポイントでブロックできるのは実用的と感じました。timeoutasyncadditionalContext などのプロパティを組み合わせることで、ガードレールとしてだけでなく監査ログや AI による柔軟な判定にも活用できそうです。

誰かの参考になれば幸いです。


関連リンク:

この記事をシェアする

関連記事