Guide to creating and managing Claude Code Hooks.
Overview
Hooks are scripts that run automatically before and after tool execution.
| Type | Timing | Use case |
|---|---|---|
| PreToolUse | Tool executionBefore | Validation, blocking dangerous operations |
| PostToolUse | Tool executionAfter | Suggestions, logging |
| Stop | At conversation end | Cleanup |
Creating a Hook
1. Create the script
#!/bin/bash
# .claude/hooks/my-hook.sh
# 環境変数
# TOOL_NAME: ツール名 (Write, Edit, Bash, etc.)
# TOOL_INPUT: JSON形式の入力パラメータ
# 終了コード
# 0: 成功(許可)
# 1: 警告(許可、メッセージ表示)
# 2: ブロック(実行を停止)
# 例: 危険なコマンドをブロック
if echo "$TOOL_INPUT" | grep -q "rm -rf /"; then
echo "🚫 危険なコマンドをブロックしました" >&2
exit 2
fi
exit 0
2. Grant execution permission
chmod +x .claude/hooks/my-hook.sh
3. Register in the settings file
// .claude/settings.local.json
{
"hooks": {
"PreToolUse": [
{
"matcher": "Bash",
"hooks": [
{
"type": "command",
"command": "/path/to/project/.claude/hooks/my-hook.sh"
}
]
}
]
}
}
Practical Hook examples
PreToolUse: Blocking dangerous operations
#!/bin/bash
# validate-dangerous-ops.sh
case "$TOOL_NAME" in
"Bash")
COMMAND=$(echo "$TOOL_INPUT" | jq -r '.command // empty')
# force push をブロック
if echo "$COMMAND" | grep -qE 'git push.*(--force|-f).*(main|master)'; then
echo "🚫 main/master への force push は禁止です" >&2
exit 2
fi
;;
"Write"|"Edit")
FILE_PATH=$(echo "$TOOL_INPUT" | jq -r '.file_path // empty')
# .env ファイル編集を警告
if echo "$FILE_PATH" | grep -qE '\.env'; then
echo "⚠️ 環境変数ファイルを編集しようとしています" >&2
exit 1 # 警告のみ
fi
;;
esac
exit 0
PostToolUse: Suggestions after commit
#!/bin/bash
# suggest-after-commit.sh
# git commit 後のみ発火
if ! echo "$TOOL_INPUT" | grep -q "git commit"; then
exit 0
fi
# 変更ファイルをチェック
CHANGED=$(git diff-tree --no-commit-id --name-only -r HEAD 2>/dev/null)
if echo "$CHANGED" | grep -q "schema"; then
echo "" >&2
echo "💡 スキーマが変更されました" >&2
echo " マイグレーションファイルの作成を検討してください" >&2
exit 1
fi
exit 0
Stop: Cleanup at session end
#!/bin/bash
# cleanup-on-stop.sh
# マージ済みブランチの確認
MERGED=$(git branch --merged main | grep -v main | grep -v '\*')
if [ -n "$MERGED" ]; then
echo "" >&2
echo "🧹 マージ済みブランチがあります:" >&2
echo "$MERGED" >&2
echo " 削除を検討してください: git branch -d <branch>" >&2
exit 1
fi
exit 0
Settings structure
{
"hooks": {
"PreToolUse": [
{
"matcher": "Write|Edit|Bash", // 正規表現でマッチ
"hooks": [
{ "type": "command", "command": "/path/to/hook.sh" }
]
}
],
"PostToolUse": [
{
"hooks": [
{ "type": "command", "command": "/path/to/hook.sh" }
]
}
],
"Stop": [
{
"hooks": [
{ "type": "command", "command": "/path/to/hook.sh" }
]
}
]
}
}
Debugging
# 直接実行してテスト
TOOL_NAME="Bash" TOOL_INPUT='{"command":"git push --force main"}' ./my-hook.sh
echo $? # 終了コード確認
Best Practices
- Non-blocking: Avoid long-running processes
- Output to stderr: Output to stderr, not stdout
- Suggestions only: Do not auto-execute; leave the decision to a human
- Parse with jq: Use jq for JSON parsing
- Fail-safe: If jq is not available, allow (exit 0)