ankuro.dev
← ブログ一覧に戻る
【Claude Code中級編 #9】Hook設定を最適化する——ifフィールドでgit操作・危険コマンドだけ反応させる
2026-03-31#Claude Code#AI#Hooks#v2.1.85

【Claude Code中級編 #9】Hook設定を最適化する——ifフィールドでgit操作・危険コマンドだけ反応させる

この記事では以下がわかる。

  • v2.1.85で追加された if フィールドの正しい書き方(実測済み)
  • Bash(git *)Write(*.ts) などの条件指定でフックの発火を絞り込む方法
  • gitログ取得・削除コマンドのガード・ファイル種別ごとの処理分岐などの実用例

Hooksはmatcherで対象ツールを絞れるが、Bashと書くとすべてのBashコマンドで毎回スクリプトが起動する。

v2.1.85で追加されたifフィールドを使えば、コマンド内容やファイルパスまで絞り込んで必要なときだけ発火させられる。プロセス起動のオーバーヘッドを減らしつつ、より精密なHook設計が可能になる。


これまでのHooksの限界

v2.1.85以前のHooksは、matcherでツール種別(BashReadWriteなど)を絞るのが限界だった。

{
  "hooks": {
    "PreToolUse": [
      {
        "matcher": "Bash",
        "hooks": [
          {
            "type": "command",
            "command": "echo 'Bash called' >> /tmp/hook.log"
          }
        ]
      }
    ]
  }
}

この設定では git status でも ls でも echo でも、すべてのBashコマンドでスクリプトが起動する。不要なプロセス起動はセッションの応答速度に影響する。


if フィールドとは

ifフィールドはpermission rule構文(allowed-toolsと同じ書き方)で発火条件を指定できる。

発火フロー:

ツール呼び出し
    ↓
matcher でツール種別チェック(Bash/Read/Write...)
    ↓
if で内容チェック(Bash(git *)...)
    ↓
条件一致 → hook実行 / 不一致 → スキップ

ifが一致しなかった場合、スクリプトは起動しない。プロセス生成コストがゼロになる。


設定の書き方

正しい構造(実測済み)

ifフィールドは内側のhookオブジェクト内に書く。

{
  "hooks": {
    "PreToolUse": [
      {
        "matcher": "Bash",
        "hooks": [
          {
            "type": "command",
            "if": "Bash(git *)",
            "command": "/path/to/script.sh"
          }
        ]
      }
    ]
  }
}

よくある間違い

ifを外側のオブジェクトに書いてしまうと条件が無視されてすべてのBash呼び出しで発火する

// ❌ これは動かない(ifが外側にある)
{
  "PreToolUse": [
    {
      "if": "Bash(git *)",
      "hooks": [{"type": "command", "command": "..."}]
    }
  ]
}

// ✅ 正しい(ifがhookオブジェクト内にある)
{
  "PreToolUse": [
    {
      "matcher": "Bash",
      "hooks": [
        {
          "type": "command",
          "if": "Bash(git *)",
          "command": "..."
        }
      ]
    }
  ]
}

外側に書いた場合、ifフィールドは黙って無視される。エラーも出ないため気づきにくい。実際にこのセッションで検証して確認した。


permission rule 構文

ifに指定できるパターンはpermission ruleと同じ形式。

パターン 意味
Bash(git *) git で始まるBashコマンド
Bash(rm *) rm で始まるBashコマンド
Bash(kubectl delete *) kubectl delete で始まるBashコマンド
Write(*.ts) .tsファイルへの書き込み
Edit(*.env*) .envを含むファイルへの編集
Read(**/secret*) secretを含むパスへの読み取り

*はワイルドカード(任意の文字列にマッチ)。**はディレクトリ区切りを含む任意のパスにマッチ。


実用例①:gitコマンド時だけ操作ログを残す

{
  "hooks": {
    "PostToolUse": [
      {
        "matcher": "Bash",
        "hooks": [
          {
            "type": "command",
            "if": "Bash(git *)",
            "command": "echo \"$(date '+%Y-%m-%d %H:%M:%S') git: $CLAUDE_TOOL_INPUT\" >> ~/.claude/git_history.log"
          }
        ]
      }
    ]
  }
}

git commitgit pushgit resetなど、gitコマンドだけをログに残せる。lscatでは起動しない。

検証結果:

  • git log --oneline -3 → ✅ 発火
  • ls /tmp | wc -l → ❌ 発火せず(スキップ)

実用例②:削除コマンド前にガードを入れる

本番環境でrm -rfkubectl deleteを誤って実行するリスクを減らす。

#!/usr/bin/env python3
# ~/.claude/hooks/guard-destructive.py
import json
import sys
import os

data = json.load(sys.stdin)
command = data.get("tool_input", {}).get("command", "")

# 本番ディレクトリへの削除はブロック
if "/prod/" in command or "--all-namespaces" in command:
    print(json.dumps({
        "decision": "block",
        "reason": f"本番環境への破壊的操作をブロック: {command}"
    }))
    sys.exit(0)

# それ以外は確認ログだけ残して通す
log_path = os.path.expanduser("~/.claude/destructive_ops.log")
with open(log_path, "a") as f:
    f.write(f"{command}\n")
{
  "hooks": {
    "PreToolUse": [
      {
        "matcher": "Bash",
        "hooks": [
          {
            "type": "command",
            "if": "Bash(rm *)",
            "command": "python3 ~/.claude/hooks/guard-destructive.py"
          },
          {
            "type": "command",
            "if": "Bash(kubectl delete *)",
            "command": "python3 ~/.claude/hooks/guard-destructive.py"
          }
        ]
      }
    ]
  }
}

同じmatcher内に複数のhookオブジェクトを並べられる。それぞれ独立したif条件を持てる。


実用例③:ファイル種別で処理を変える

TypeScriptファイルを書き込んだときだけlintを走らせる。

{
  "hooks": {
    "PostToolUse": [
      {
        "matcher": "Write",
        "hooks": [
          {
            "type": "command",
            "if": "Write(*.ts)",
            "command": "cd \"$CLAUDE_PROJECT_DIR\" && npx tsc --noEmit 2>&1 | head -20"
          }
        ]
      }
    ]
  }
}

.tsファイルの書き込み後だけ型チェックが走る。.json.mdの保存では起動しない。

.envファイルを編集したときに警告を出す例:

{
  "hooks": {
    "PreToolUse": [
      {
        "matcher": "Edit",
        "hooks": [
          {
            "type": "command",
            "if": "Edit(*.env*)",
            "command": "echo '⚠️ .envファイルを編集します。シークレットのコミットに注意してください。'"
          }
        ]
      }
    ]
  }
}

matcher と if の使い分け

matcher if
役割 ツール種別で絞る コマンド・パス内容で絞る
書く場所 外側のオブジェクト 内側のhookオブジェクト
"Bash" "Write" "Bash(git *)" "Write(*.ts)"
省略時の動作 全ツールに反応 常に発火(条件なし)

両方組み合わせるのがベスト:

matcher: "Bash"   → BashではないRead/Writeをスキップ
if: "Bash(git *)" → Bashの中でもgit以外をスキップ

2段階でフィルタリングすることで、不要なプロセス起動を最小化できる。


まとめ

  • ifフィールドはv2.1.85で追加されたHooksの条件付き実行機能
  • 書く場所は内側のhookオブジェクト内(外側に書くと無視される)
  • permission rule構文(Bash(git *)Write(*.ts)など)で発火条件を指定
  • matcherと組み合わせることでオーバーヘッドを最小化
  • 実測済み:Bash(git *)はgitコマンドのみに発火、lsなどでは発火しない

次回は長期セッションでトークンを無駄にしない「コンテキスト管理の技術」を扱う。


第8回:CwdChanged——ディレクトリ移動で環境変数を自動切り替える第10回:コンテキスト管理の技術——トークンを無駄にしない