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? で pip、pip3 をブロック |
| コマンドと特定の引数の組み合わせでブロック |
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だけを使っている方も、複数のエージェントを使い分けている方も、ぜひ試してみてください。
Discussion