1
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Claude Code の PreToolUse hook で危険な Bash コマンドを自動ブロックする実装 — permissionDecision と exit code 2 の使い分け、matcher 正規表現の3つのハマりどころ【2026】

1
Posted at

はじめに / 対象と前提

Claude Code で複数のプロジェクトを自動運転させていると、rm -rfgit push --force、あるいは知らないホストへの curl をエージェントがうっかり実行してしまわないか不安になる。設定画面で毎回許可を求めれば安全だが、非対話(headless)実行ではそもそも人間が確認プロンプトに答えられない。

この記事は以下を前提にする。

  • Claude Code v2.x 系(hooks の PreToolUse イベントが存在するバージョン)
  • Node.js 22.x / macOS or Linux
  • 「Bash ツールを呼ぶ直前に、コマンド文字列を検査して危険なら止めたい」というニーズがある人向け

settings.jsonpermissions.allow/deny によるワイルドカード制御はすでに知っている前提で、それだけでは足りないケース(動的な判定ロジックが要る場合)にフォーカスする。

TL;DR

  • PreToolUse hook は Bash ツール呼び出しの直前に割り込んで、コマンド文字列を任意のロジックで検査できる
  • ブロック方法は2種類ある: exit code 2(簡易)と JSON の permissionDecision(細かい制御)。混在させると片方が無視される
  • matcher はツール名(Bash など)にしかマッチしない。コマンド文字列自体のパターンマッチは hook スクリプト側で自分で書く必要がある
  • headless(-p オプション)実行では ask は自動的に deny 扱いになる

手順 / 動かし方

1. hook スクリプトを用意する

~/.claude/hooks/block-dangerous-bash.py として以下を置く。stdin から JSON が渡ってくるので、tool_input.command を取り出して正規表現でチェックする。

#!/usr/bin/env python3
import json
import re
import sys

DANGEROUS_PATTERNS = [
    r"\brm\s+-rf\s+/",
    r"\bgit\s+push\s+.*--force\b",
    r"\bgit\s+reset\s+--hard\b",
    r"\bcurl\s+.*\|\s*sh\b",
]

def main():
    payload = json.load(sys.stdin)
    tool_name = payload.get("tool_name", "")
    command = payload.get("tool_input", {}).get("command", "")

    if tool_name != "Bash":
        # matcher で絞っていても、念のため二重チェック
        print(json.dumps({}))
        return

    for pattern in DANGEROUS_PATTERNS:
        if re.search(pattern, command):
            output = {
                "hookSpecificOutput": {
                    "hookEventName": "PreToolUse",
                    "permissionDecision": "deny",
                    "permissionDecisionReason": f"危険パターンに一致: {pattern}",
                }
            }
            print(json.dumps(output))
            return

    print(json.dumps({}))

if __name__ == "__main__":
    main()

実行権限を付けておく。

chmod +x ~/.claude/hooks/block-dangerous-bash.py

2. settings.json に登録する

~/.claude/settings.json(またはプロジェクトの .claude/settings.json)に以下を追加する。

{
  "hooks": {
    "PreToolUse": [
      {
        "matcher": "Bash",
        "hooks": [
          {
            "type": "command",
            "command": "python3 ~/.claude/hooks/block-dangerous-bash.py"
          }
        ]
      }
    ]
  }
}

matcher"Bash" を指定すると、Bash ツールが呼ばれるたびにこの hook が起動する。Write/Edit など他のツールでは発火しない。

3. 動作確認

Claude Code のセッション内で意図的に危険なコマンドを投げてみる。

git push origin main --force

hook が deny を返すと、Claude 側には「このコマンドは実行できませんでした」という結果が返り、git push は実際には走らない。ログで確認するなら stderr にデバッグ出力を足しておくと分かりやすい。

sys.stderr.write(f"[hook] checked command: {command}\n")

claude --debug で起動すると、どの hook がどう判定したかがコンソールに出るので、初回セットアップ時はこれで確認するのが早い。

ハマりどころ

1. matcher はコマンド文字列にはマッチしない

matcherツール名(Bash, Write, Edit など)にしかマッチしない。「rm を含む Bash コマンドだけに絞りたい」といった正規表現を matcher に書いても効かない。コマンド文字列そのものの判定は、必ず hook スクリプト内で tool_input.command をパースして行う必要がある。最初にこれを誤解して「matcher に rm.* と書けば絞れるはず」とハマった。

2. exit code 方式と JSON 出力方式を混在させると片方が無視される

ブロックには2つの方法がある。

  • exit code 2: stderr に書いたメッセージがそのままブロック理由として使われる、シンプルな方式
  • JSON 出力: stdout に hookSpecificOutput.permissionDecision を含む JSON を返す、allow / deny / ask を細かく制御できる方式

厄介なのは、この2つを同じスクリプト内で中途半端に混在させたときの挙動だ。exit code 2 で終了しつつ stdout にも JSON を書いていたケースでは、JSON 側の permissionDecision は評価されず exit code 側の判定が優先された。どちらか一方の方式に統一するのが安全で、細かい理由文言を返したいなら JSON 方式、単純に止めたいだけなら exit code 2 で十分。

3. headless 実行では ask が自動的に deny になる

対話セッションでは permissionDecision: "ask" を返すと人間に確認ダイアログが出る。しかし claude -p "..." の headless 実行では確認相手がいないため、ask自動的に deny として扱われる。「グレーゾーンのコマンドは ask にして人間の判断を仰ぐ」という設計にしていると、無人実行時に想定外に全部ブロックされてハマる。無人ジョブで動かす hook は ask を使わず、allowdeny の二値で判定を書き切る設計にしておいたほうが事故がない。

背景・補足

PreToolUse は「ツール実行前」、PostToolUse は「ツール実行後」に発火する。lint や型チェックを自動で走らせたいなら PostToolUse(Write/Edit の後に fire)、実行そのものを止めたいなら PreToolUse という住み分けになる。同じ hooks の仕組みでもイベントが違えば用途もまったく別なので、「hook を設定した」だけで安心せず、どのイベントに登録したかを都度確認する癖をつけておくとよい。

まとめ

  • PreToolUse hook で Bash コマンドを実行前に検査し、危険なパターンを自動ブロックできる
  • ブロック方式は exit code 2 と JSON 出力の2種類、混在させず統一する
  • matcher はツール名のみが対象、コマンド文字列の判定は自前で書く
  • headless 実行では ask が自動 deny になるため、無人ジョブでは allow/deny の二値設計にする
  • 導入後は claude --debug で実際に判定ログを確認してから運用に載せるのが安全
1
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?