AI企業(こびと社)を個人で運営しながら、Claude Code を毎日使って試行錯誤している。この記事は実際に動かして得た知見をまとめたもの。
動作確認環境: macOS Sequoia 15.x / Claude Code(最新版)/ zsh
Claude Code に「.env をコミットしようとしたら止める」「セッション終了時に日次ログを記録する」といった自動的な制御を追加できることを知っていますか?
これが Claude Code の hooks 機能です。
この記事では、hooks の仕組みと実践的な使い方を解説します。実際に動いているコードを元にしています。
hooks とは何か
hooks は「Claude Code が特定のアクションを実行する前後に、シェルスクリプトを自動で走らせる仕組み」です。
Claude Code がツールを使う
↓
[PreToolUse hook] ← ここに検査スクリプトを仕込む
↓
ツール実行
↓
[PostToolUse hook] ← 後処理をここで走らせる
↓
結果を返す
hooks がなければ: Claude Code が .env をコミットしようとしても止められない。
hooks があれば: git commit を検知したら自動でシークレット検査スクリプトを実行し、問題があればコミットをブロックできる。
設定できる4つのイベント
.claude/settings.json の hooks フィールドで設定します。
| イベント | タイミング |
|---|---|
PreToolUse |
Claude がツールを使う前 |
PostToolUse |
Claude がツールを使った後 |
PreCompact |
/compact(コンテキスト圧縮)の前
|
Stop |
Claude がセッションを終了する前 |
設定の書き方
.claude/settings.json:
{
"hooks": {
"PreToolUse": [
{
"matcher": "Bash",
"hooks": [
{
"type": "command",
"command": "bash .claude/hooks/pre-commit-check.sh",
"when": "tool_input.command.startsWith('git commit')"
}
]
}
],
"Stop": [
{
"hooks": [
{
"type": "command",
"command": "bash .claude/hooks/session-stop.sh"
}
]
}
]
}
}
matcher: どのツールが対象か(Bash, Write, Edit, Read など)。省略すると全ツールが対象。
when: 追加の条件(JavaScript の式)。tool_input.command でコマンド文字列を参照できます。
command: 実行するシェルコマンド。スクリプトファイルを指定するのがおすすめです。
実践例1: git commit 前のシークレット検査
.env の誤コミットや API キーの漏洩を自動で防ぎます。
.claude/hooks/pre-commit-check.sh:
#!/usr/bin/env bash
set -euo pipefail
STAGED_FILES=$(git diff --cached --name-only --diff-filter=ACM 2>/dev/null || true)
if [ -z "$STAGED_FILES" ]; then
exit 0
fi
echo "==> Checking for secrets..."
SECRET_PATTERNS=(
'sk-[a-zA-Z0-9]{20,}' # Anthropic API key
'sk-ant-[a-zA-Z0-9-]{20,}' # Anthropic API key (older format)
'ghp_[a-zA-Z0-9]{30,}' # GitHub token
'AKIA[0-9A-Z]{16}' # AWS Access Key
'-----BEGIN [A-Z ]*PRIVATE KEY-----'
)
EXIT_CODE=0
for pattern in "${SECRET_PATTERNS[@]}"; do
while IFS= read -r file; do
[ -z "$file" ] && continue
[ ! -f "$file" ] && continue
if grep -nE "$pattern" "$file" > /dev/null 2>&1; then
echo "::error file=$file::Possible secret detected"
EXIT_CODE=2
fi
done <<< "$STAGED_FILES"
done
# revenue ディレクトリの混入チェック
if echo "$STAGED_FILES" | grep -qE '^logs/revenue/|/credentials/'; then
echo "::error::Revenue/credentials files must not be committed."
EXIT_CODE=2
fi
# CLAUDE.md の行数チェック
if [ -f "CLAUDE.md" ]; then
LINE_COUNT=$(wc -l < CLAUDE.md)
if [ "$LINE_COUNT" -gt 200 ]; then
echo "::warning::CLAUDE.md is $LINE_COUNT lines (recommended: <200)"
fi
fi
if [ "$EXIT_CODE" -eq 2 ]; then
echo "Commit blocked."
exit 2
fi
echo "==> All checks passed."
exit 0
動作イメージ:
Claude: git commit -m "feat: add new feature"
↓
[pre-commit-check.sh が自動起動]
==> Checking for secrets...
::error file=.env::Possible secret detected
Commit blocked.
↓
Claude: コミット中止。.env を確認してください
Claude Code 側はブロックを受け取ると、コミットを中止してユーザーに報告します。
実践例2: セッション終了時の日次ログ確認
Claude Code が会話を終了しようとするとき、日次ログの記録状況を確認します。
.claude/hooks/session-stop.sh:
#!/usr/bin/env bash
TODAY=$(date +%Y-%m-%d)
NOW=$(date +%H:%M)
DAILY_LOG="logs/daily/${TODAY}.md"
# 日次ログがなければ雛形を作る
if [ ! -f "$DAILY_LOG" ]; then
mkdir -p "$(dirname "$DAILY_LOG")"
cat > "$DAILY_LOG" << 'EOF'
---
title: Daily Log — TODAY
type: daily-log
---
# 今日やったこと
- (未記入)
# 次セッションへの引き継ぎ
- (未記入)
EOF
sed -i '' "s/TODAY/$TODAY/" "$DAILY_LOG"
fi
# session-close が記録済みか確認
if grep -q "セッション終了記録" "$DAILY_LOG"; then
echo "セッション終了 ($NOW) — ログ記録済み ✓"
else
echo ""
echo "⚠ セッション終了 ($NOW) — /session-close が未実行です"
echo "次回セッション開始時に前回分を補足してください。"
fi
exit 0
重要: Stop hook のスクリプトは exit 0 で終わる必要があります。非ゼロで終了すると Claude Code がセッション終了を拒否します。
実践例3: Write ツールで特定ディレクトリへの書き込みを監視する
{
"PostToolUse": [
{
"matcher": "Write",
"hooks": [
{
"type": "command",
"command": "bash .claude/hooks/post-write-check.sh",
"when": "tool_input.path.startsWith('src/')"
}
]
}
]
}
# .claude/hooks/post-write-check.sh
#!/usr/bin/env bash
# src/ に書き込みがあったら ruff で自動チェック
if command -v ruff &> /dev/null; then
ruff check src/ --quiet || echo "::warning::Ruff found issues in src/"
fi
exit 0
src/ への Write が完了したら自動で ruff が走ります。コード品質チェックをコミット前ではなく書き込み直後に走らせたいときに便利です。
hook スクリプトの注意点
1. 終了コードの意味
| コード | 意味 | 主な用途 |
|---|---|---|
0 |
成功 | 通常完了 |
1 |
警告(続行) | 問題はあるが止めない |
2 |
ブロック(中止) |
PreToolUse でツール実行を止める |
PostToolUse と Stop は終了コードに関わらず動作済みのアクションを取り消せません。
2. 標準出力は Claude が読む
hook の echo 出力は Claude Code の会話に表示されます。
echo "::error::シークレットを検出しました" # エラーとして表示
echo "::warning::警告: 200行超えています" # 警告として表示
echo "普通のメッセージ" # 通常のメッセージ
Claude はこの出力を元に次のアクションを判断します。具体的なメッセージを書くと Claude が正確に対応できます。
3. タイムアウトに注意
hook スクリプトは長時間実行すると Claude のレスポンスが遅くなります。PreToolUse hook は特に軽量に保ちましょう。目安は5秒以内。
4. when の条件式
when フィールドには JavaScript の式が書けます。
"when": "tool_input.command.startsWith('git commit')"
"when": "tool_input.path.endsWith('.env')"
"when": "tool_input.command.includes('rm -rf')"
||(OR)や &&(AND)も使えます。
実際の運用パターン
パターン1: 権限ゲート型
「この操作は hook で確認が取れた場合だけ通す」設計。
{
"PreToolUse": [
{
"matcher": "Bash",
"hooks": [
{
"type": "command",
"command": "bash .claude/hooks/check-destructive.sh",
"when": "tool_input.command.includes('rm ') || tool_input.command.includes('DROP TABLE')"
}
]
}
]
}
check-destructive.sh が exit 2 を返せばコマンドは実行されません。
パターン2: 監視型
操作後に状態を記録したい場合。
{
"PostToolUse": [
{
"matcher": "Bash",
"hooks": [
{
"type": "command",
"command": "echo \"$(date +%Y-%m-%dT%H:%M:%S) BASH: $TOOL_INPUT\" >> logs/tool-history.log",
"when": "tool_input.command.startsWith('git ')"
}
]
}
]
}
git 操作をすべてログに残せます。
パターン3: 自動整形型
{
"PostToolUse": [
{
"matcher": "Write",
"hooks": [
{
"type": "command",
"command": "[ '${tool_input.path}' = '*.py' ] && uv run ruff format '${tool_input.path}' 2>/dev/null || true"
}
]
}
]
}
Python ファイルへの Write 後に自動でフォーマット。
まとめ
Claude Code hooks を使うと、AI の行動に「仕組みレベルの制御」を追加できます。
| 使い方 | hook の種類 | 実装 |
|---|---|---|
| コミット前のシークレット検査 | PreToolUse (Bash) | git commit を when で絞る |
| コード品質チェック | PostToolUse (Write) | src/ への書き込みをトリガーに |
| セッション終了の記録確認 | Stop | 日次ログの状態を確認 |
| 破壊的操作のブロック | PreToolUse (Bash) | exit 2 で中止 |
hooks は「信頼して任せる範囲を広げるためのセーフティネット」です。
Claude Code に自律的に動かしたい場合ほど、止める仕組みをきちんと作るのが重要です。
この記事は Zenn でも近日公開予定。良かったらフォローしてください → Zenn: kobito-co