🦀

Claude Code, Curosr, Windsurf のHooks設定を10倍楽にする claw-hooks

に公開

はじめに

Claude CodeやCursorなどのAIコーディングエージェントでHooksを設定しようとすると、複雑なPython/Bashスクリプトを書く必要があります。

例えば「rmコマンドをブロックする」だけでも:

  • JSONのパース処理
  • コマンド文字列の判定ロジック
  • 適切なレスポンス形式での出力

これらを含む複雑なスクリプトが必要になります。

ましてや複数のコーディングエージェントを使い分けている場合、エージェントごとにJSON構造が異なるため、同じ機能を実現するスクリプトを複数書く必要が出てきます。

この記事では、スクリプト不要シンプルなTOML設定だけでHooksを定義できる claw-hooks を紹介します。

TL;DR

  • スクリプト不要: Python/Bashを書かずにTOMLだけでHooksを定義
  • 1行で危険コマンドブロック: kill_block = true, rm_block = true で完了(AST解析でsudo/pipeも検出)
  • 拡張子フック: [extension_hooks] でファイル保存時にフォーマッター/リンター自動実行
  • 高速: Rust製シングルバイナリ、起動時間 <10ms
  • 設定の一元化: 複数エージェントを使っていても1つのTOMLで共通管理--formatフラグで切替)

対象読者

  • AIコーディングエージェント(Claude Code、Cursor、Windsurf等)を使っているエンジニア
  • Hooksを使いたいがスクリプトを書くのが面倒な人
  • 危険コマンド(rm、kill等)をブロックしたい人
  • ファイル保存時に自動フォーマットを実行したい人
  • 複数のエージェントを使い分けていて、Hook設定を統一したい

環境

  • OS: macOS, Linux, Windows
  • 依存: なし(シングルバイナリ)
  • 対応エージェント: Claude Code, Cursor, Windsurf

CodexもHooks実装されないかな・・・

従来のHooks設定の問題点

問題1: 単一エージェントでもスクリプトが複雑

まず、1つのエージェントだけを使っている場合でも、Hooks設定には複雑なスクリプトが必要です。

rmコマンドをブロックするだけでも複雑なスクリプトが必要

Claude Code向けに`rm`をブロックするPythonスクリプト(AIで生成):
#!/usr/bin/env python3
import json
import sys

def main():
    input_data = json.loads(sys.stdin.read())
    tool_name = input_data.get("tool_name", "")
    tool_input = input_data.get("tool_input", {})

    if tool_name == "Bash":
        command = tool_input.get("command", "")
        dangerous = ["rm ", "rm -", "rmdir"]
        if any(cmd in command for cmd in dangerous):
            result = {
                "decision": "block",
                "message": "🚫 Dangerous command blocked"
            }
            print(json.dumps(result))
            sys.exit(2)

    print(json.dumps({"decision": "approve"}))
    sys.exit(0)

if __name__ == "__main__":
    main()

拡張子別のフォーマッター実行はさらに複雑

例(AIで生成):
#!/usr/bin/env python3
import json
import sys
import subprocess
import os

def main():
    input_data = json.loads(sys.stdin.read())
    tool_name = input_data.get("tool_name", "")
    tool_input = input_data.get("tool_input", {})

    if tool_name in ["Write", "Edit", "MultiEdit"]:
        file_path = tool_input.get("file_path", "")
        ext = os.path.splitext(file_path)[1]

        commands = {
            ".rs": ["rustfmt {}"],
            ".py": ["ruff format {}", "ruff check --fix {}"],
            ".ts": ["biome format --write {}", "biome lint --write {}"],
        }

        if ext in commands:
            for cmd in commands[ext]:
                subprocess.run(cmd.format(file_path), shell=True)

    print(json.dumps({"decision": "approve"}))

if __name__ == "__main__":
    main()

1つのエージェントだけでも、これだけのスクリプトを書く必要があります。

問題2: 複数エージェントだとさらに大変

複数のエージェントを使い分けている場合、エージェントごとにJSON構造が異なるため、同じ機能でも別々のスクリプトを書く必要があります。

コマンド実行前のフック(PreToolUse / beforeShellExecution / pre_run_command)で渡されるJSONを比較してみましょう:

Claude Code (PreToolUse):

{
  "session_id": "abc123",
  "hook_event_name": "PreToolUse",
  "tool_name": "Bash",
  "tool_input": {
    "command": "npm install lodash"
  }
}

Cursor (beforeShellExecution):

{
  "conversation_id": "conv-456",
  "hook_event_name": "beforeShellExecution",
  "command": "npm install lodash",
  "cwd": "/Users/you/project"
}

Windsurf (pre_run_command):

{
  "agent_action_name": "pre_run_command",
  "trajectory_id": "traj-789",
  "tool_info": {
    "command_line": "npm install lodash",
    "cwd": "/Users/you/project"
  }
}

上記の複雑なスクリプトを、Cursor用・Windsurf用と3つ書くのは現実的ではありません。

claw-hooksによる解決

claw-hooksは、これらの問題を同時に解決します:

  • 単一エージェントでも: 複雑なスクリプトがシンプルなTOML設定
  • 複数エージェントでも: 1つの設定ファイルで全エージェント対応

インストール

# Homebrew (macOS/Linux)
brew install owayo/claw-hooks/claw-hooks

# または Releases からバイナリをダウンロード

設定ファイル生成

claw-hooks init
# ~/.config/claw-hooks/config.toml が生成される

TOMLで簡潔に設定

# 危険コマンドのブロック(2行で完了)
rm_block = true
rm_block_message = "🚫 ユーザーに直接実行を依頼してください"

kill_block = true
kill_block_message = "🚫 ユーザーに直接実行を依頼してください"

dd_block = true
dd_block_message = "🚫 ユーザーに直接実行を依頼してください"

# カスタムフィルター(正規表現対応)
[[custom_filters]]
command = "yarn"
message = "`yarn`の代わりに`pnpm`を使用してください"

# argsモード: コマンド名(正規表現) + 引数で絞り込み
[[custom_filters]]
command = "npm"
args = ["install", "i", "add"]  # npm install, npm i, npm add をブロック
message = "`npm`の代わりに`pnpm`を使用してください"

[[custom_filters]]
command = "pip3?"               # pip と pip3 両方にマッチ
args = ["install", "uninstall"]
message = "`uv pip`を使用してください"

[[custom_filters]]
command = "docker"
args = ["rm", "rmi", "system prune"]
message = "ユーザーに直接実行を依頼してください"

# 拡張子フック(保存時に自動実行)
[extension_hooks]
".rs" = ["rustfmt {file}"]
".go" = ["gofmt -w {file}", "golangci-lint run {file}"]
".py" = ["ruff format {file}", "ruff check --fix {file}"]
".ts" = ["biome format --write {file}", "biome lint --write {file}"]
".tsx" = ["biome format --write {file}", "biome lint --write {file}"]
".css" = ["biome format --write {file}", "biome lint --write {file}"]

# 終了フック(エージェント終了時に通知)
[[stop_hooks]]
command = "afplay /System/Library/Sounds/Glass.aiff"  # macOS向け通知音

各エージェントへの設定

設定ファイル(TOML)は共通で、エージェントごとに--formatフラグを変えるだけです。
Claude Codeだけを使っている場合はフラグすら不要です。

Claude Code (~/.claude/settings.json):

{
  "hooks": {
    "PreToolUse": [
      { "matcher": "Bash", "hooks": [{ "type": "command", "command": "claw-hooks hook" }] }
    ],
    "PostToolUse": [
      { "matcher": "Write|Edit|MultiEdit", "hooks": [{ "type": "command", "command": "claw-hooks hook" }] }
    ],
    "Stop": [
      { "matcher": "", "hooks": [{ "type": "command", "command": "claw-hooks hook" }] }
    ]
  }
}

Cursor (~/.cursor/hooks.json):

{
  "version": 1,
  "hooks": {
    "beforeShellExecution": [{ "command": "claw-hooks hook --format cursor" }],
    "afterFileEdit": [{ "command": "claw-hooks hook --format cursor" }],
    "stop": [{ "command": "claw-hooks hook --format cursor" }]
  }
}

Windsurf (~/.codeium/windsurf/hooks.json):

{
  "hooks": {
    "pre_run_command": [{ "command": "claw-hooks hook --format windsurf" }],
    "post_write_code": [{ "command": "claw-hooks hook --format windsurf" }],
    "post_cascade_response": [{ "command": "claw-hooks hook --format windsurf" }]
  }
}

なぜclaw-hooksが優れているか

1. スクリプト不要でシンプル

従来の複雑なPythonスクリプトが、シンプルなTOML設定で置き換えられます:

# これだけでrmコマンドをブロック
rm_block = true
rm_block_message = "🚫 ユーザーに直接実行を依頼してください"

2. AST解析による正確なコマンド検出

tree-sitter-bashを使用したAST解析により、文字列マッチでは検出できないパターンも捕捉:

# すべてブロックされる
rm -rf /tmp
sudo rm file.txt
cd /tmp && rm -f *.log
echo "setup" | xargs rm
bash -c "rm secret.txt"
# これはブロックされない(引数内の文字列)
echo "rm is dangerous"
grep "rm -rf" docs.txt

3. カスタムフィルターの2つのモード

カスタムフィルターは目的に応じて2つのモードを使い分けられます:

# カスタムフィルター(正規表現対応)
[[custom_filters]]
command = "yarn"
message = "`yarn`の代わりに`pnpm`を使用してください"

# argsモード: コマンド名(正規表現) + 引数で絞り込み
[[custom_filters]]
command = "pip3?"               # pip と pip3 両方にマッチ
args = ["install", "uninstall"]
message = "`uv pip`を使用してください"
モード 設定方法 用途
コマンド全ブロック commandのみ pip3?pippip3 をブロック
コマンドと特定の引数の組み合わせでブロック command + args サブコマンド単位の制御(npm installのみブロック)

4. 複数エージェントでも設定は1つ

claw-hooksは各エージェントのJSON形式を内部で自動変換するため、設定ファイルは1つだけで済みます:

5. 比較表

機能 従来のHooks claw-hooks
危険コマンドブロック 複雑なPythonスクリプト rm_block = true
カスタムフィルター スクリプト追加 [[custom_filters]]
サブコマンド制御 複雑な引数パース argsフィールドで簡潔に
拡張子フック 複雑なファイル判定 [extension_hooks] マップ
マルチエージェント エージェントごとにスクリプト --format フラグで切替
終了通知 別途スクリプト作成 [[stop_hooks]]
sudo/pipe検出 ほぼ不可能 AST解析で自動対応

実践例

動作確認

# 安全なコマンド(許可される)
echo '{"hook_event_name":"PreToolUse","tool_name":"Bash","tool_input":{"command":"git status"}}' | claw-hooks hook
# Output: {"decision":"approve"}

# 危険なコマンド(ブロックされる)
echo '{"hook_event_name":"PreToolUse","tool_name":"Bash","tool_input":{"command":"rm -rf /"}}' | claw-hooks hook
# Output: {"decision":"block","message":"🚫 Use safe-rm instead..."}

チェインコマンドの検出

# セミコロンで連結されたyarnもブロック
echo '{"hook_event_name":"PreToolUse","tool_name":"Bash","tool_input":{"command":"echo install; yarn install"}}' | claw-hooks hook
# → {"decision":"block","message":"Use `pnpm` instead of `yarn`"}

# 引用符内は無視される
echo '{"hook_event_name":"PreToolUse","tool_name":"Bash","tool_input":{"command":"echo \"not yarn\"; pnpm install"}}' | claw-hooks hook
# → {"decision":"approve"}

argsモードによるサブコマンド制御

# npm install はブロック(argsに含まれる)
echo '{"hook_event_name":"PreToolUse","tool_name":"Bash","tool_input":{"command":"npm install lodash"}}' | claw-hooks hook
# → {"decision":"block","message":"Use `pnpm` instead of `npm`"}

# npm run build は許可(argsに含まれない)
echo '{"hook_event_name":"PreToolUse","tool_name":"Bash","tool_input":{"command":"npm run build"}}' | claw-hooks hook
# → {"decision":"approve"}

# pip3 install もブロック(command = "pip3?" で pip/pip3 両方にマッチ)
echo '{"hook_event_name":"PreToolUse","tool_name":"Bash","tool_input":{"command":"pip3 install requests"}}' | claw-hooks hook
# → {"decision":"block","message":"Use `uv pip` instead"}

パフォーマンス

指標
起動時間 <10ms

Rustで書かれているため、Pythonスクリプトと比較して起動オーバーヘッドがほぼゼロです。

まとめ

claw-hooksを使うことで:

単一エージェントでも:

  • スクリプト不要: 複雑なPythonがシンプルなTOMLに
  • 正確な検出: AST解析でsudo/pipe/subshellも捕捉
  • 柔軟なフィルター: argsモードでサブコマンド単位の制御が可能
  • 高速: <10msの起動時間でストレスなし

複数エージェントを使っているなら:

  • 設定の一元化: 1つのTOMLファイルで全エージェント対応
  • フォーマット自動変換: --formatフラグだけで各エージェントに対応

Claude Codeだけを使っている方も、複数のエージェントを使い分けている方も、ぜひ試してみてください。

https://github.com/owayo/claw-hooks

Discussion