| name | hooks-reference |
| description | Claude Codeフックの詳細仕様、設計原則、設定方法。フック、hook、PreToolUse、Stop時に使用。 |
フックリファレンス
Claude Codeフックの詳細仕様と設計原則。
フック出力フォーマット
フックはJSON形式で結果を返す:
| フィールド | 説明 |
|---|---|
decision |
"approve" または "block" |
reason |
ブロック理由(Claudeに送信) |
systemMessage |
ユーザー表示メッセージ |
出力パターン設計
フックの出力は以下のパターンに従う(不要な出力を最小化):
| 状況 | JSON出力 | exit code | 理由 |
|---|---|---|---|
| 対象外 | なし | 0 | 対象外で出力するとログが煩雑 |
| 許可 | なし | 0 | 正常動作時は沈黙が原則 |
| ブロック | {"decision": "block", "reason": "..."} |
0 | 明示的な拒否メッセージが必要 |
| 通知 | {"decision": "approve", "systemMessage": "..."} |
0 | 許可しつつ情報を伝達 |
ヘルパー関数(lib/results.py):
make_block_result(hook_name, reason): ブロック結果を生成make_approve_result(hook_name, message=None): 許可結果を生成
共通ライブラリ関数の詳細仕様(必読)
フック実装前に必ず確認すること。これを怠るとレビューで指摘される。
make_block_result(hook_name, reason)
自動的に行われる処理(手動で行う必要なし):
| 処理 | 詳細 |
|---|---|
| ログ記録 | log_hook_execution(hook_name, "block", reason) を内部で呼び出す(Issue #2023) |
| プレフィックス付与 | reasonに [{hook_name}] を自動追加 |
| 継続ヒント付与 | reasonに CONTINUATION_HINT を自動追加 |
| systemMessage生成 | ユーザー表示用のsystemMessageを自動生成 |
| stderr出力 | ❌ {hook_name}: {first_line} をstderrに出力 |
アンチパターン(やってはいけない):
# ❌ 悪い例: 重複呼び出し
log_hook_execution("my-hook", "block", reason) # 不要(make_block_resultが呼ぶ)
result = make_block_result("my-hook", reason)
# ❌ 悪い例: 重複プレフィックス
reason = f"[my-hook] {actual_reason}" # 不要(make_block_resultが付与)
result = make_block_result("my-hook", reason)
# ✅ 良い例: reasonのみを渡す
result = make_block_result("my-hook", "操作がブロックされました")
make_approve_result(hook_name, message=None)
仕様:
decision: "approve"とsystemMessageを含む辞書を返す- messageがNoneの場合、
✅ {hook_name}: OKがsystemMessageになる reasonフィールドは含まれない(blockと異なる)
その他のヘルパー関数
| 関数 | 用途 |
|---|---|
print_continue_and_log_skip(hook_name, reason) |
対象外時の早期リターン(PreToolUse/PostToolUse用) |
print_approve_and_log_skip(hook_name, reason) |
対象外時の早期リターン(Stop hook用) |
check_skip_env(hook_name, env_var) |
SKIP環境変数のチェックとログ記録 |
parse_hook_input() (lib/session.py)
フック入力の標準的な読み取り方法。以下を自動で行う:
- stdinからJSONを読み取りパース
session_idがあれば自動でset_hook_session_id()を呼び出す- パースした辞書を返す(エラー時は空辞書)
戻り値構造(フックタイプ別):
| フックタイプ | 主要フィールド |
|---|---|
| PreToolUse / PostToolUse | tool_name, tool_input, session_id, cwd |
| UserPromptSubmit | user_prompt, session_id, cwd |
| SessionStart | session_id, cwd, source, transcript_path |
| Stop | stop_hook_active, session_id, cwd |
使用例:
from lib.session import parse_hook_input
def main():
hook_input = parse_hook_input() # session_idが自動設定される
tool_name = hook_input.get("tool_name")
tool_input = hook_input.get("tool_input", {})
command = tool_input.get("command", "")
# ... フックロジック
アンチパターン:
# ❌ 悪い例: 手動でJSONパースしてsession_id設定を忘れる
data = json.loads(sys.stdin.read())
# session_idが設定されない → ログが正しくセッションに紐づかない
# ✅ 良い例: parse_hook_input()を使用
hook_input = parse_hook_input() # session_id自動設定
HookContextへの移行(Issue #2449)
get_claude_session_id() はグローバル状態を使用するため、テスト困難性とスレッドセーフ性の問題がある。新しいフックは HookContext パターンを使用すること。
移行パターン:
# ❌ 旧パターン: グローバル状態を使用
from lib.session import parse_hook_input, get_claude_session_id
def main():
hook_input = parse_hook_input()
session_id = get_claude_session_id() # グローバル状態から取得
# ...
# ✅ 新パターン: HookContext(DI)を使用
from lib.session import parse_hook_input, create_hook_context
def main():
hook_input = parse_hook_input()
ctx = create_hook_context(hook_input)
session_id = ctx.get_session_id() # コンテキストから取得
# ...
メリット:
| 観点 | 旧パターン | 新パターン |
|---|---|---|
| テスト | モック困難(グローバル状態) | コンテキスト注入で容易 |
| スレッドセーフ | 問題あり | 問題なし |
| 依存関係 | 暗黙的 | 明示的 |
移行状況:
get_claude_session_id()はdeprecated(SHOW_SESSION_DEPRECATION=1で警告表示)- 既存フックは段階的に移行中
- 新規フックは必ず
HookContextパターンを使用すること
フック実装前チェックリスト(共通ライブラリ)
-
lib/results.pyのソースを確認したか -
lib/session.pyのparse_hook_input()を使用しているか -
create_hook_context()を使用しているか(get_claude_session_id()は非推奨) - 使用する関数の副作用(ログ記録、プレフィックス付与)を理解したか
- 重複処理(手動ログ記録、手動プレフィックス)をしていないか
設計判断の理由:
- JSON出力はClaudeへのメッセージ表示用 → 重要な情報(ブロック・通知)のみ
- ログ記録は監査・分析用 → 全実行を記録(対象外・許可含む)
- 「全て記録して、利用時にフィルター」の原則
ログ記録パターン:
| 状況 | decision値 | 例 |
|---|---|---|
| 対象外 | skip |
"Not a merge command" |
| 許可 | approve |
"All checks passed" |
| ブロック | block |
"auto-merge blocked" |
systemMessage出力例
各フックのsystemMessage出力例。新規フック開発時の参考に。
task-start-checklist.py:
📋 **タスク開始前の確認チェックリスト**
以下の点を確認してからタスクを開始してください:
**要件確認**:
[ ] 要件は明確か?曖昧な点があれば質問する
[ ] ユーザーの意図を正しく理解しているか?
**設計判断**:
[ ] 設計上の選択肢がある場合、ユーザーに確認する
[ ] 既存のコードパターン・規約を把握しているか?
💡 不明点があれば、実装前に必ず質問してください。
dependency-check-reminder.py:
📦 **依存関係追加を検出**
パッケージ `react-query` を追加しようとしています。
**最新情報を確認してください:**
1. **Context7**: `react-query` のドキュメントを参照
- `mcp__context7__resolve-library-id` でライブラリIDを取得
- `mcp__context7__get-library-docs` でドキュメントを取得
2. **Web検索**: 最新バージョン・変更履歴を確認
- 「react-query latest version」で検索
💡 古いAPIや非推奨メソッドの使用を防ぐため、最新情報の確認を推奨します。
open-issue-reminder.py:
🚨 **高優先度Issue(優先対応必須)**:
→ #123: 本番環境でログイン失敗 [P1, bug]
📋 **未アサインのオープンIssue** (対応検討してください):
- #456: ダークモード対応 [enhancement]
- #789: パフォーマンス改善 [P2]
詳細: `gh issue list --state open`
出力設計ガイドライン:
| 項目 | 推奨 |
|---|---|
| タイトル | 絵文字 + 太字で目立たせる |
| 構造 | 箇条書き/チェックリスト形式 |
| 長さ | 5-15行(長すぎると読まれない) |
| アクション | 具体的な次のステップを提示 |
PreToolUse フック一覧
Edit/Write ブロック
- トリガー: ファイル編集前
- 動作: main/masterでの編集をブロック、worktree外を警告
オープンIssueリマインド (open-issue-reminder.py)
- 目的: 未アサインIssue表示、競合防止
- 動作: セッション開始時(1時間間隔)に最初のBashでトリガー
タスク開始チェックリスト (task-start-checklist.py)
- 目的: タスク開始時の要件・設計確認漏れ防止
- 動作: セッション開始時(1時間間隔)に最初のEdit/Write/Bashでトリガー
- 表示内容: 要件確認、設計判断、影響範囲、前提条件のチェックリスト
- ブロック: しない(systemMessageでリマインド表示のみ)
依存関係チェックリマインド (dependency-check-reminder.py)
- 目的: 依存関係追加時にContext7/Web検索を促す
- 動作:
pnpm add,npm install,pip install等のコマンド検出時にトリガー - 表示内容: Context7でのドキュメント確認、Web検索での最新情報確認を促すメッセージ
- ブロック: しない(systemMessageでリマインド表示のみ)
- 重複防止: 同じパッケージには1セッション1回のみ表示
Issue自動アサイン (issue-auto-assign.py)
- 目的: 複数エージェントのIssue競合防止
- 動作:
git worktree addでブランチ名からIssue番号を検出し自動assign - パターン:
feature/issue-123-desc,fix/123-bug,#123-feature
PRスコープチェック (pr-scope-check.py)
- 目的: 1 Issue = 1 PR ルール強制
- 動作:
gh pr createで複数Issue参照をブロック
Skill使用リマインド・強制フック
worktree作成・PR作成時のSkill参照を促す2つの補完的なフック。
workflow-skill-reminder.py(リマインド型)
- 目的: worktree/PR作成時に
development-workflowSkill参照をリマインド - トリガー:
git worktree add,gh pr create検出時 - 動作: 警告のみ(systemMessageでリマインド表示、ブロックしない)
- 関連Issue: #2387
出力例:
📚 workflow-skill-reminder: worktree作成が検出されました。
【development-workflow Skill 参照リマインダー】
worktree作成時は `development-workflow` Skill を参照してください。
**確認すべき内容:**
□ worktree作成直後のチェック(main最新との差分確認)
□ `--lock` オプションの使用(他エージェントの削除防止)
...
skill-usage-reminder.py(強制型)
- 目的: Skill使用なしでのworktree/PR作成をブロック
- トリガー:
git worktree add,gh pr create検出時 - 動作: セッション中のtranscriptを確認し、必要なSkillが使用されていなければブロック
- 関連Issue: #2355
補完関係
両フックは以下の補完関係にある:
| フック | チェック内容 | 動作 |
|---|---|---|
workflow-skill-reminder.py |
リマインド表示 | 「Skillを参照すべき」とリマインド |
skill-usage-reminder.py |
Skill未使用時にブロック | Skill未使用ならブロック |
フロー:
git worktree addを実行しようとすると、同じ PreToolUse フェーズで2つのフックが起動するworkflow-skill-reminder.py: 常に"approve"を返しつつ、systemMessageで「development-workflow Skillを参照してください」とリマインドを表示skill-usage-reminder.py: transcript を確認し、指定された Skill が未参照であれば"block"を返し、参照済みであれば"approve"を返す
両フックは同じコマンドに対して並行して独立に実行され、workflow-skill-reminder.py のリマインドと skill-usage-reminder.py のブロック可否判定の結果が組み合わされて Claude に渡される。
設計意図: リマインドで気づかせ、無視した場合はブロックで強制。2段階の防御で「手順が身についている」という誤った判断を防止。
マージ安全性チェック (merge-check.py)
4つのチェック:
gh pr merge --autoをブロックrequested_reviewersにCopilot/Codexがいたらブロック- Issue参照なしで却下されたコメントをブロック
- コメントなしResolveをブロック
却下検出キーワード: 「範囲外」「軽微」「out of scope」「defer」
CI待機チェック (ci-wait-check.py)
- 目的: CI監視を
ci-monitor.pyに一元化 - ブロック:
gh pr checks --watch,gh pr view --json mergeStateStatus等
Codex CLIレビューチェック
- logger:
codex review実行時にブランチ・コミットを記録 - check:
gh pr create/git push時に現在コミットがレビュー済みか確認
Pythonコードチェック (python-lint-check.py)
- 目的: CI前にPythonスタイル違反を検知
- 動作:
git commitでステージされた.pyをuvx ruffでチェック
フック設計チェック (hooks-design-check.py)
- 目的: フック間の責務重複防止、品質チェック
- 動作: 新規フック追加時にSRPチェックリストを警告表示、
log_hook_execution()未使用をブロック
UI確認チェック (ui-check-reminder.py)
- 目的: UI変更後の目視確認漏れ防止
- 対象:
locales/*.json,components/**/*.tsx,routes/**/*.tsx,index.css - 確認記録:
python3 .claude/scripts/confirm-ui-check.py
Markdownサイズチェック (markdown-size-check.py)
- 目的: Markdownファイル肥大化防止
- 上限: 40KB(Claude Codeパフォーマンス影響閾値)
Worktree削除前チェック (worktree-removal-check.py)
- 目的: worktree削除前にアクティブ作業を検出し、セッション競合・破壊を防止
- トリガー:
git worktree removeコマンド検出時
2段階チェック:
| チェック | 対象 | --force でバイパス |
|---|---|---|
| cwdチェック | 現在の作業ディレクトリがworktree内にあるか | 不可(常にブロック) |
| アクティブ作業チェック | 最新コミット・未コミット変更・stash | 可能 |
cwdチェックの重要性:
cwdがworktree内にある状態で削除すると、以降の全Bashコマンドが ENOENT エラーで失敗する。
セッション全体が壊れるため、--force でも絶対にバイパスできない。
設計レビュー結果:
| 観点 | 判断 | 理由 |
|---|---|---|
| 並行性 | 問題なし | 各セッションは独立してcwdをチェック |
| エッジケース | 対応済み | symlink→resolve()、permission denied→fail-close |
| 依存関係 | 問題なし | cwdチェックはgitコマンド不使用(純粋なパス比較) |
| 状態管理 | 適切 | OSErrorでfail-close(ブロック側に倒す) |
| セキュリティ | 対応済み | resolve()でパストラバーサル対策 |
| 拡張性 | 良好 | 各チェックが独立関数として実装 |
Fail-Close設計:
# check_cwd_inside_worktree() の例
try:
cwd = Path.cwd().resolve()
# ... パス比較 ...
except OSError:
# cwdが取得できない場合は安全側に倒す
return True # ブロック
不確実な状況(OSError等)では常に「ブロック」を選択。誤ブロックは回復可能だが、誤許可によるセッション破壊は回復不可能なため。
PostToolUse フック一覧
Issue AIレビュー (issue-ai-review.py)
- 目的: Issue作成後に自動でAIレビュー(Gemini/Codex)を実行
- トリガー:
gh issue create成功後 - 動作: バックグラウンドでGemini/Codexレビューを実行し、結果をIssueコメントとして投稿
- ブロック: しない(PostToolUseで非ブロッキング実行)
Worktree自動セットアップ (worktree-auto-setup.py)
- 目的: worktree作成後の依存関係自動インストール
- トリガー:
git worktree add成功後 - 動作:
setup-worktree.shを自動実行(pnpm install等) - ブロック: しない(PostToolUseで非ブロッキング実行)
ブロック改善リマインダー (block-improvement-reminder.py)
- 目的: 同一フックが連続ブロックした際にフック改善を促す
- トリガー: Bashツール実行後、セッションのブロック履歴を確認
- 動作: 同一フックが3回連続でブロックしていたら、改善策をsystemMessageで表示
- ブロック: しない(systemMessageでリマインド表示のみ)
- 重複防止: 同じフックには1セッション1回のみ表示
- 関連Issue: #2432
検討すべき改善策の例:
- SKIP環境変数のサポート追加
- 拒否メッセージの改善(具体的な解決策を提示)
- 誤検知パターンの修正
出力例:
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
💡 フック改善リマインダー: merge-check
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
このセッションで `merge-check` が3回連続でブロックしています。
**検討すべき改善策:**
1. **SKIP環境変数のサポート追加**
- `SKIP_MERGE_CHECK=1` でバイパス可能に
2. **拒否メッセージの改善**
- 具体的な解決策を提示
- 何をすべきか明確に説明
3. **誤検知パターンの修正**
- 正当なケースをブロックしていないか確認
- 検出ロジックの精度を改善
詳細は `hooks-reference` Skill を参照してください。
Stop フック
cwd-check
- 目的: カレントディレクトリ消失検知
- 動作: セッション終了時にcwd存在確認
git-status-check
- 目的: 未コミット変更検知
- 動作: mainに未コミット変更があれば警告
reflection-prompt
五省ベースの自己評価(prompt型):
- 評価基準: 要件理解、実装品質、検証、対応、効率 + 仕組み化
- 動作:
- 重大な未完了タスク →
block - 教訓が見つかったが仕組み化されていない →
block - タスク完了 →
approve
- 重大な未完了タスク →
Session ID取得の仕組み
フックはセッションIDを使用してセッション固有の状態を管理する。
取得優先順位
Issue #777でClaude Codeが直接session_idを提供するようになったため、シンプルな2段階:
| 優先度 | ソース | 説明 |
|---|---|---|
| 1 | hook/statusline input JSON .session_id |
Claude Codeが提供(最も信頼性が高い) |
| 2 | fallback | Python: ppid-{PPID} / Bash: 空文字列 |
廃止済み: 環境変数(CLAUDE_SESSION_ID, CLAUDE_CONVERSATION_ID)、marker file
実装
Python(正式実装): common.py の get_claude_session_id()
from common import get_claude_session_id
session_id = get_claude_session_id()
Bash: statusline.sh の get_session_id()
(パフォーマンス上、Python実装と同様の方針でbashに実装。ただしfallback時の挙動は異なり、Pythonは ppid-{PPID}、Bashは空文字列を返す)
DEBUGログ
CLAUDE_DEBUG=1 環境変数を設定すると、取得元がstderrに出力される:
[session_id] source=hook_input, value=abc123...
関連Issue
- Issue #734: セッションごとのstate file分離
- Issue #756: hook inputからsession_id取得
- Issue #777: Claude Codeによるsession_id提供
- Issue #779: 取得ロジックの統一・ドキュメント化
プロセス間状態共有
重要な制約
フックは別プロセスで実行されるため、以下の制約がある:
| 方式 | 動作 | 使用可否 |
|---|---|---|
| グローバル変数 | プロセス終了で消失 | ❌ |
| モジュールレベル辞書 | プロセス間で共有不可 | ❌ |
| ファイルベース | 永続化・共有可能 | ✅ |
| 環境変数 | 読み取り専用 | △ (読み取りのみ) |
よくある間違い(Issue #1617):
# ❌ 動作しない: プロセス終了で消失
_cache = {}
def get_cached_value(key):
if key not in _cache:
_cache[key] = expensive_operation()
return _cache[key]
実装前チェックリスト
フック/スクリプト実装前に確認:
- プロセス間で状態共有が必要か?
- 必要ならファイルベース永続化を使用
- 複数プロセスからの同時書き込みは? → ファイルロック検討
ファイルベース永続化パターン
推奨: JSON Lines形式(.jsonl)で追記
from __future__ import annotations
import json
from pathlib import Path
from common import METRICS_LOG_DIR
# ディレクトリを事前に作成
METRICS_LOG_DIR.mkdir(parents=True, exist_ok=True)
LOG_FILE = METRICS_LOG_DIR / "my-data.jsonl"
def append_event(event: dict) -> None:
"""イベントを追記(必要に応じてファイルロックを併用)"""
# 注意:
# - 小規模なログの追記であれば、通常はファイルシステムの書き込みアトミック性で
# 十分なことが多く、必ずしもロックを導入する必要はありません。
# - 複数プロセスから高頻度に同一ファイルへ書き込む場合は、fcntl や filelock などに
# よるファイルロック、あるいは専用のログ集約プロセスや外部ストレージの利用を
# 検討してください。
with open(LOG_FILE, "a") as f:
f.write(json.dumps(event) + "\n")
def read_events() -> list[dict]:
"""全イベントを読み込み"""
if not LOG_FILE.exists():
return []
events = []
# 大きなログファイルでもメモリ効率よく読み込むため、行単位で処理する
with open(LOG_FILE, "r") as f:
for line in f:
if line.strip():
events.append(json.loads(line))
return events
既存の実装例:
.claude/logs/metrics/block-patterns.jsonl- ブロック→成功パターン.claude/logs/execution/hook-execution.log- フック実行ログ
関連Issue
- Issue #1617: インメモリ状態管理の問題
- Issue #1634: プロセス間状態共有の教訓
フック設計原則
- 単一責任: 1フック = 1責務
- 疎結合: フック間の依存最小化
- パス解決:
$CLAUDE_PROJECT_DIRを使用 - SKIP環境変数: 全ブロッキングフックは
SKIP_*環境変数でバイパス可能にする
SKIP環境変数の命名規則:
| フック | 環境変数 |
|---|---|
| worktree-removal-check | SKIP_WORKTREE_CHECK |
| issue-incomplete-close-check | SKIP_INCOMPLETE_CHECK |
| issue-review-response-check | SKIP_REVIEW_RESPONSE |
| planning-enforcement | SKIP_PLAN |
| codex-review-check | SKIP_CODEX_REVIEW |
実装パターン:
from common import extract_inline_skip_env, is_skip_env_enabled
SKIP_HOOK_NAME_ENV = "SKIP_HOOK_NAME"
# main()関数内でチェック(エクスポートされた環境変数とインライン両方をサポート)
# 1. エクスポートされた環境変数をチェック
if is_skip_env_enabled(os.environ.get(SKIP_HOOK_NAME_ENV)):
log_hook_execution("hook-name", "skip", "SKIP_HOOK_NAME=1: チェックをスキップ")
print(json.dumps({"decision": "approve"}))
return
# 2. インライン環境変数をチェック(例: SKIP_HOOK_NAME=1 gh issue close)
inline_value = extract_inline_skip_env(command, SKIP_HOOK_NAME_ENV)
if is_skip_env_enabled(inline_value):
log_hook_execution("hook-name", "skip", "SKIP_HOOK_NAME=1: チェックをスキップ(インライン)")
print(json.dumps({"decision": "approve"}))
return
アンチパターン:
- ❌ 既存フックに「ついでに」別機能を追加
- ❌ 1フックで複数の無関係なチェック
- ❌ ブロッキングフックでSKIP環境変数をサポートしない
- ✅ 新責務は新フックとして実装
警告 vs ブロックの判断基準
フック設計時に「警告で許可」か「ブロックで拒否」かを判断する基準。
| 条件 | 選択 | 理由 |
|---|---|---|
| 副作用のある操作を防ぎたい | ブロック | 警告後もコマンドが実行され、副作用は発生してしまう |
| 操作失敗でフロー問題が発生 | ブロック | 例: exit code 1でPostToolUseフックがスキップされる |
| 情報提供のみで操作は許可 | 警告 | ユーザーに判断を委ねる場合 |
| 軽微な問題の通知 | 警告 | 操作自体は問題なく完了する場合 |
実例(Issue #2286, #2293):
worktree内から gh pr merge --delete-branch を実行すると:
- マージは成功する(副作用発生)
- ブランチ削除は失敗する(使用中のため)
- exit code 1が返る
- PostToolUseフックがスキップされる
当初は「警告」で実装したが、これでは問題が解決しない:
- 警告が表示されても、コマンドは実行される
- 副作用(マージ)は既に発生済み
- PostToolUseフック(振り返り等)が発火しない
判断フローチャート:
- 副作用のある操作を防ぎたいか?
- はい:
ブロック - いいえ:
- 操作失敗時にフロー問題が起きるか?
- はい:
ブロック - いいえ:
警告(または通知なし)
- はい:
- 操作失敗時にフロー問題が起きるか?
- はい:
コマンド実行パターン
フック内で外部コマンドを実行する際の標準パターン。
shell=True vs shell=False の使い分け
| 状況 | shell | 理由 |
|---|---|---|
リダイレクト(2>&1)を含む |
True |
シェルがリダイレクトを解釈 |
パイプ(|)を含む |
True |
シェルがパイプを解釈 |
環境変数展開($VAR)が必要 |
True |
シェルが展開 |
| 上記以外 | False |
セキュリティと明確さのため |
推奨パターン
✅ shell=False(デフォルト):
# リストでコマンドを渡す(推奨)
result = subprocess.run(
["gh", "pr", "list", "--json", "number,title"],
capture_output=True,
text=True,
timeout=30
)
✅ shell=True(リダイレクト・パイプが必要な場合):
# 文字列でコマンドを渡す
result = subprocess.run(
"gh pr list --search 'author:@me' 2>&1",
shell=True,
capture_output=True,
text=True,
timeout=30
)
アンチパターン
❌ shell=Falseでリダイレクトを含む:
# 悪い例: 2>&1がghの引数として渡される
result = subprocess.run(
"gh pr list 2>&1".split(), # split()でリストに変換
capture_output=True,
text=True
)
# エラー: "2>&1" が gh のコマンド引数として解釈される
❌ shell=Falseで文字列を渡す:
# 悪い例: shell=Falseで文字列は非推奨
result = subprocess.run(
"gh pr list", # 文字列
shell=False, # shell=False
...
)
# エラー: FileNotFoundError: [Errno 2] No such file or directory: 'gh pr list'
common.pyのヘルパー関数
フック共通モジュールにはコマンド実行のヘルパーがあります:
from common import run_command
# 基本的な使用法(shell=False、リストで渡す)
result = run_command(["gh", "pr", "list", "--json", "number"])
# シェル機能が必要な場合
result = run_command("gh pr list 2>&1 | grep error", shell=True)
関連Issue
- Issue #1106: locked-worktree-guardがシェルリダイレクトを誤認識
フック実装チェックリスト
新しいフックを実装する際は、以下のチェックリストを確認する。
正規表現設計
コマンド検出の正規表現を設計する際の考慮事項:
- 環境変数プレフィックス:
SKIP_PLAN=1 git worktree addのようなインライン環境変数 - パイプ連結:
cmd1 | cmd2パターン - シェル連結:
&&,;,||によるコマンド連結 - 引用符内のコマンド:
echo "git commit"のような文字列内のコマンド(誤検知防止)lib/strings.pyのstrip_quoted_strings()を使用して引用符内を除去してから検査
- サブシェル:
$(cmd)や`cmd`内のコマンド
参考パターン:
# 環境変数プレフィックスとシェル連結を考慮した例
# (?:^|&&|\|\||;|\s+) でコマンド開始位置を特定(\s+ で複数空白に対応)
pattern = r"(?:^|&&|\|\||;|\s+)(?:\w+=\S+\s+)*git\s+worktree\s+add"
入力処理
- 空入力:
tool_inputが空の場合の処理 - 不正形式: JSON構造が期待と異なる場合
- 必須フィールド欠落:
tool_input.command等が存在しない場合 - Fail-Close設計: 不確実な状況ではブロック側に倒す
- hook_cwd取得: cwdに依存するフックは
input_data.get("cwd")を使用(Issue #1172)
hook_cwdパターン:
input_data = parse_hook_input()
hook_cwd = input_data.get("cwd") # Claude Codeが提供するセッションの実cwd
# hook_cwd を base_cwd パラメータとして渡す(環境変数より優先される)
cwd = get_effective_cwd(command, base_cwd=hook_cwd)
テスト
- 正常系: 期待するコマンドを正しく検出
- 異常系: 不正入力でクラッシュしない
- エッジケース: 環境変数プレフィックス、パイプ連結等
- 誤検知防止: 類似コマンドや引用符内を誤検出しない
- テスト手法の確認:
run_hook(subprocess)かdirect callか事前確認
テスト手法の選択:
| 方式 | モック可否 | 用途 |
|---|---|---|
run_hook()(subprocess) |
❌ 効かない | E2Eテスト、実際の動作確認 |
hook_module.main() 直接呼び出し |
✅ 効く | ユニットテスト、例外ハンドリング確認 |
# モックが必要なテストは直接呼び出しを使用
from unittest.mock import patch
def mock_func(*args, **kwargs):
raise FileNotFoundError("simulated error")
with patch.object(hook_module, "get_effective_cwd", side_effect=mock_func):
hook_module.main() # 例外発生時の動作をテスト
出力フォーマット設計
systemMessage出力を含むフックを実装する場合のチェックリスト:
- 出力目的の明確化: 何を伝えたいか1文で説明できるか
- 出力構造の設計: 絵文字タイトル + 箇条書き/チェックリスト形式
- 出力長の確認: 5-15行を目安(長すぎると読まれない)
- アクション提示: 次に何をすべきか具体的に示す
- hooks-referenceへの追記: 出力例をドキュメントに追加
出力テンプレート:
def get_message() -> str:
"""Generate the systemMessage content."""
lines = [
"📋 **[タイトル]**",
"",
"[説明文]",
"",
"**[セクション1]**:",
" - [項目1]",
" - [項目2]",
"",
"💡 [アクション/ヒント]",
]
return "\n".join(lines)
精度向上のポイント:
- 研究によると、出力例を含めると精度が向上する場合がある
- 曖昧な指示より具体的なフォーマット指定が効果的
- 箇条書き/チェックリスト形式は解釈のばらつきを減らす
Skill整合性チェック(Issue #1196)
フック設計時に関連Skillのルールとの整合性を確認する。これを怠ると、Skillに記載されたルールがフックで強制されず、ルール違反が発生する。
必須チェック項目:
- 関連Skill特定: このフックが関連するSkillを特定したか?
- 例:
bug-issue-creation-guard.py→code-reviewSkill - 例:
merge-check.py→code-reviewSkill - 例:
worktree-removal-check.py→development-workflowSkill
- 例:
- ルール網羅: 関連Skillに記載されたルールを全てカバーしているか?
- 例:
code-reviewSkillに「テスト不足は同じPRで対応」とあれば、「テスト」パターンも検出必須
- 例:
- パターン確認: フックの検出パターンがSkillの記述と一致するか?
- 例: Skillに「バグ、テスト不足、エッジケース」とあれば、全てパターンに含める
確認手順:
- フックの目的を明確化(何を防止/検出するか)
- 関連するSkillを
.claude/skills/から特定 - Skillに記載されたルール/条件を抽出
- フックのパターン/ロジックが全ルールをカバーしているか確認
- 不足があればフックを拡張
失敗事例:
| 問題 | 原因 | 対策 |
|---|---|---|
| テスト不足Issueがフックをすり抜け | code-review Skillの「テスト不足」ルールを検出パターンに含めていなかった |
パターンに「テスト」を追加 |
| エッジケースIssueがフックをすり抜け | code-review Skillの「エッジケース」ルールを検出パターンに含めていなかった |
パターンに「エッジケース」を追加 |
| コード品質Issueがフックをすり抜け | Skillルール整合性チェックなしでフックを設計した | Skillルール整合性チェックを導入 |
docstringテンプレート:
"""
Hook to [目的].
Related Skills:
- code-review: [関連ルール]
- development-workflow: [関連ルール]
Detection patterns based on Skill rules:
- Pattern A: [Skillルール1]
- Pattern B: [Skillルール2]
"""
関連Issue
- Issue #1085: 正規表現で環境変数プレフィックスを考慮していなかった事例
- Issue #1172: hook_cwdを使用していなかったためcwd検出が失敗
- Issue #1196: フック設計時のSkillルール整合性チェック
パターン検出フック作成ガイドライン
キーワードリストや正規表現パターンを使用してテキストを検出するフック(例: defer-keyword-check.py)を作成・変更する際のガイドライン。
実データ分析の重要性
仮説ベースでパターンを選定すると:
- 誤検知が多い: 実際には問題ないケースをブロック
- 漏れが多い: 本当に検出すべきパターンを見逃す
- メンテナンス負荷: 後から修正が必要になる
必須チェックリスト
パターン検出フックの作成・変更時は以下を確認:
実データソースを特定したか
- GitHub PR comments:
gh api repos/{owner}/{repo}/pulls/{pr}/comments - Issue comments:
gh api repos/{owner}/{repo}/issues/{issue}/comments - セッションログ:
~/.claude/logs/*.jsonl
- GitHub PR comments:
実データからパターンを抽出したか
- 仮説ベースではなく実際のデータを分析
- 頻度・コンテキストを確認
- 最低10件以上の実例を収集
作成したパターンをテストしたか
- 検出率(実際に検出すべきものを検出できているか)
- 誤検知率(検出すべきでないものを検出していないか)
- 目標: 検出率 > 90%、誤検知率 < 10%
分析ツール
.claude/scripts/analyze-pattern-data.py を使用:
# パターン検索(実データからマッチを確認)
python3 .claude/scripts/analyze-pattern-data.py search \
--pattern "後で|将来|フォローアップ" \
--show-matches
# 頻度分析(パターンの出現頻度を確認)
python3 .claude/scripts/analyze-pattern-data.py analyze \
--pattern "スコープ外" \
--days 30
# パターンリスト検証(複数パターンの精度を一括チェック)
python3 .claude/scripts/analyze-pattern-data.py validate \
--patterns-file my-patterns.txt
実装時のベストプラクティス
パターンリスト変数の命名:
# 明確な命名で目的を示す DEFER_KEYWORDS = [...] # 「後で」系キーワード SCOPE_OUT_PATTERNS = [...] # スコープ外パターン実データ分析の証跡をコメントに残す:
# 実データ分析: PR comments from 2025-12-30 # 検出対象: Issue参照なしで使われると問題になるパターン # 分析結果: 30件中28件検出、誤検知2件 DEFER_KEYWORDS = [...]除外コンテキストを考慮:
# 誤検知防止: コードブロック、ドキュメント参照、ルール説明 EXCLUDE_CONTEXTS = [ r"```", # コードブロック r"AGENTS\.md", # ドキュメント参照 ]
自動検出
hook-change-detector.py がパターン検出フックの変更を検知し、実データ分析チェックリストをリマインドします。
検出条件:
*_KEYWORDS,*_PATTERNS,*_REGEX変数を含む- 正規表現パターンリストを含む
re.compile()を含む
関連Issue
- Issue #1910: AskUserQuestion検出フック
- Issue #1911: 「後で」キーワード検出フック
- Issue #1912: パターン検出フック作成時の実データ分析強制
ブロックパターン追加時のチェックリスト
新しいブロックパターンをフックに追加する際のチェックリスト。引用符内での誤検知問題を踏まえた防止策。
誤検知防止
引用符内のパターン:
--body "..."や--title "..."内での言及を除外- 対策: 引用符内のコンテンツを除去するヘルパー関数を使用(下記実装例参照)
- ❌ 悪い例:
if "gh run watch" in command: - ✅ 良い例:
if "gh run watch" in strip_quoted_content(command):
コメント内:
# command は使わないのような文脈- 対策: 行頭が
#の場合は無視する処理を検討
- 対策: 行頭が
変数展開:
$commandが対象パターンを含む場合- 対策:
\bで単語境界を明確にする
- 対策:
パイプ/リダイレクト:
echo "..." | grep ...- 対策:
strip_quoted_content()でカバー
- 対策:
パターン設計
- 正規表現を使用する場合、エスケープ漏れがないか
- 大文字小文字の区別が必要か確認
- 複数行コマンド対応が必要か
テスト
- 正常系: ブロックすべきコマンドがブロックされる
- 誤検知テスト: 引用符内での言及がブロックされない
def test_approves_quoted_mention(self): """引用符内でのパターン言及は承認される.""" command = 'gh pr comment --body "使用禁止: gh run watch"' result = should_block(command) assert result is False - テスト追加先:
.claude/hooks/tests/test_<hook名>.py
実装例
import re
# NOTE: この関数はドキュメント用の簡略化サンプルです。
# 実際のフック実装では、.claude/hooks/ci-wait-check.py 内の strip_quoted_content を参照してください。
# そちらは文字単位のパースにより、エスケープされた引用符や未閉じ引用符などのエッジケースに対応しています。
def strip_quoted_content(text: str) -> str:
"""引用符内のコンテンツを簡易的に除去する.
ダブルクォート/シングルクォートで囲まれた内容を空文字に置き換える。
例: 'gh pr comment --body "使用禁止: gh run watch"' -> 'gh pr comment --body ""'
注意: この実装は正規表現による簡略版であり、以下のエッジケースには対応していません:
- 文字列外のエスケープされた引用符(例: \"foo\")
- 閉じられていない引用符
実運用時は .claude/hooks/ci-wait-check.py の実装を使用してください。
"""
return re.sub(r'(["\'])(?:\\.|(?!\1).)*\1', r'\1\1', text)
def should_block(command: str) -> bool:
"""Check if command should be blocked."""
# 引用符内のコンテンツを除去してからチェック
clean_command = strip_quoted_content(command)
# ブロック対象パターン
blocked_patterns = [
r"\bgh\s+run\s+watch\b",
r"\bgh\s+pr\s+checks\s+--watch\b",
]
for pattern in blocked_patterns:
if re.search(pattern, clean_command):
return True
return False
関連Issue
- Issue #1621: ブロックパターンチェックリストの追加
git rev-list差分チェックのガイドライン
git rev-list でブランチ間の差分をチェックする場合、以下の3ケースを必ずテストする。
必須テストケース
| ケース | 状態 | 期待動作 |
|---|---|---|
| ahead | ローカルがリモートより進んでいる | 通常は許可 |
| behind | ローカルがリモートより遅れている | ブロックまたは警告 |
| same | 同一コミット | 許可 |
アンチパターン
❌ ハッシュ不一致でブロック:
# 悪い例: aheadでも誤ってブロック
if local_hash != remote_hash:
return block()
✅ behind_countでブロック:
# 良い例: behindの場合のみブロック
behind_count = get_behind_count() # git rev-list main..origin/main
if behind_count > 0:
return block()
テストコード例
def test_approves_when_local_is_ahead(self):
"""ローカルが進んでいる場合は許可"""
with patch.object(hook, "get_behind_count", return_value=0):
# behind=0 means local is same or ahead
result = hook.main()
self.assertEqual(result["decision"], "approve")
def test_blocks_when_local_is_behind(self):
"""ローカルが遅れている場合はブロック"""
with patch.object(hook, "get_behind_count", return_value=3):
result = hook.main()
self.assertEqual(result["decision"], "block")
def test_approves_when_same(self):
"""同一コミットの場合は許可(behind_count=0で判定)"""
# Note: behind_countベースの実装では、aheadとsameは同じ条件(behind_count=0)
with patch.object(hook, "get_behind_count", return_value=0):
result = hook.main()
self.assertEqual(result["decision"], "approve")
関連Issue
- Issue #755: worktree-main-freshness-checkでの発見
- Issue #760: 本ガイドライン追加
フックコード更新タイミング
フックはセッション開始時にロードされ、セッション中の修正は反映されない。
| タイミング | 動作 |
|---|---|
| セッション開始時 | フックコードがロードされる |
| セッション中 | フックを修正・マージしても現セッションには反映されない |
| 次セッション | 修正済みコードが適用される |
影響
- フックの修正後も、現セッションでは旧コードが動作し続ける
- 修正を検証する場合、新しいセッションを開始する必要がある
- 誤検知が発生しても、現セッション内で「なぜまだ動くのか」と混乱しやすい
対処法
- 修正検証: 新セッションで動作確認
- 現セッション: 誤検知は無視して作業続行(修正済みなら問題なし)
関連Issue
- Issue #2120: lesson-issue-checkの誤検知修正(この問題を発見したきっかけ)
- Issue #2124: 本ドキュメント追加
CWD問題の対処
cd でフックが見つからなくなる問題:
- フック設定:
$CLAUDE_PROJECT_DIRを使用(設定済み) cdを避ける:-C/--dirオプションを使用- ✅
pnpm add xxx -C frontend - ❌
cd frontend && pnpm add xxx
- ✅
- 問題発生時: セッション再起動が必要
Block評価・改善サイクル
フックによるブロックが妥当だったか評価し、誤検知の場合はフックを改善するサイクル。
ワークフロー
[Block発生] → [ログ記録] → [評価] → [分析] → [改善]
1. Blockログの確認
# 最近のブロック一覧
python3 .claude/scripts/block-evaluator.py list
# 特定のblockを詳細表示
python3 .claude/scripts/block-evaluator.py evaluate <block_id>
2. Block妥当性の評価
評価オプション:
valid- ブロックは正しかった(本来止めるべき操作)false_positive- 誤検知(止めるべきではなかった)unclear- 判断できない
# 対話的に評価
python3 .claude/scripts/block-evaluator.py evaluate <block_id>
# ワンライナーで評価
python3 .claude/scripts/block-evaluator.py evaluate <block_id> \
-e false_positive \
-r "テストファイルなのにブロックされた" \
-i "テストファイルを除外すべき"
3. 評価サマリーの確認
python3 .claude/scripts/block-evaluator.py summary
出力例:
Hook Valid False+ Unclear FP Rate
----------------------------------------------------------------------
ci-wait-check 5 3 0 37.5%
codex-review-check 10 1 0 9.1%
4. 誤検知パターンの分析
python3 .claude/scripts/analyze-false-positives.py
# 特定のフックのみ分析
python3 .claude/scripts/analyze-false-positives.py --hook ci-wait-check
5. フック改善
分析結果に基づいてフックを改善:
- 改善用worktree作成
- フックコード修正
- テスト追加
- PR作成・マージ
ログファイル
| ファイル | 内容 |
|---|---|
.claude/logs/hook-execution.log |
全フック実行ログ |
.claude/logs/block-evaluations.log |
Block評価記録 |
評価タイミング
- 推奨: セッション終了時に未評価ブロックを確認
- 必須: 「誤検知では?」と感じた時に即評価
AIレビュー対応ガイドライン
Copilot/Codexレビューで頻繁に指摘されるパターンと、事前検出の仕組み。
よくある指摘パターン
| パターン | 事前検出 | 対処法 |
|---|---|---|
| docstring不足 | ruff D101-D103 | pyproject.tomlで有効化済み |
| シグネチャ変更時のテスト未更新 | signature_change_check.py | pre-pushで警告 |
| 署名なしのスレッド解決 | resolve-thread-guard | 署名フォーマット必須 |
docstringルール(D101-D103)
pyproject.tomlで以下のruffルールを有効化:
[tool.ruff.lint]
select = [
# ... 既存ルール ...
"D101", # Missing docstring in public class
"D102", # Missing docstring in public method
"D103", # Missing docstring in public function
]
[tool.ruff.lint.pydocstyle]
convention = "google"
ローカル確認:
uvx ruff@0.14.9 check .claude/hooks/ .claude/scripts/ --select D101,D102,D103
シグネチャ変更チェック
signature_change_check.py (pre-push hook):
- 関数シグネチャ(引数・戻り値)の変更を検出
- 対応テストファイルが更新されていない場合に警告
- 警告のみ(ブロックしない)
Known Limitations:
- 単一行の関数定義のみ検出(複数行は未対応)
- 関数名の変更は検出対象外
レビュースレッド解決の署名
resolve-thread-guard で必須化されている署名フォーマット:
| パターン | 例 |
|---|---|
| 範囲外 | [対象外] 本PRの範囲外のため対応しない |
| 軽微 | [軽微] タイポ修正のため今回は見送り |
| 対応済み | [対応済み] コミット abc1234 で修正 |
| 別Issue | [別Issue] #123 で対応予定 |
署名なしでResolveすると merge-check.py でブロックされる。
関連Issue
- Issue #1107: Copilotレビューエラー時の対応手順
- Issue #1108: 関数シグネチャ変更時のテスト更新チェック
- Issue #1113: Copilotレビュー指摘パターンの事前検出
フックテンプレート
新規フック作成時のボイラープレート。再作業を防ぐため、テストから先に書く(TDD)。
1. テストファイルを先に作成
# .claude/hooks/tests/test_my_hook.py
"""Tests for my-hook.py"""
import json
from unittest.mock import patch
import pytest
class TestMyHook:
"""Test cases for my-hook."""
def test_approves_when_not_target_command(self):
"""対象外コマンドは許可される."""
# Given
hook_input = {"tool_name": "Bash", "tool_input": {"command": "ls -la"}}
# When
with patch("sys.stdin") as mock_stdin:
mock_stdin.read.return_value = json.dumps(hook_input)
# フックをインポートして実行
# ...
# Then: 出力なし(対象外)
def test_blocks_when_target_command(self):
"""対象コマンドはブロックされる."""
# Given
hook_input = {"tool_name": "Bash", "tool_input": {"command": "target command"}}
# When / Then
# ...
def test_approves_when_skip_env_set(self):
"""SKIP_MY_HOOK=1 でスキップ."""
# Given
hook_input = {"tool_name": "Bash", "tool_input": {"command": "SKIP_MY_HOOK=1 target command"}}
# When / Then
# ...
def test_handles_empty_input(self):
"""空入力でクラッシュしない."""
# Given
hook_input = {}
# When / Then: 例外なく処理
def test_handles_invalid_json(self):
"""不正JSONでクラッシュしない."""
# ...
2. フック本体を実装
#!/usr/bin/env python3
"""My hook description.
What it does:
- Check A
- Block B
"""
import json
import os
import sys
from common import (
extract_inline_skip_env,
is_skip_env_enabled,
log_hook_execution,
make_block_result,
)
SKIP_ENV = "SKIP_MY_HOOK"
def should_block(command: str) -> tuple[bool, str]:
"""Check if command should be blocked.
Args:
command: The command string to check.
Returns:
Tuple of (should_block, reason).
"""
# 対象コマンドのチェックロジック
if "target pattern" in command:
return True, "この操作はブロックされました。"
return False, ""
def main() -> None:
"""Entry point for the hook."""
try:
data = json.load(sys.stdin)
except json.JSONDecodeError:
# Fail-open: JSONエラーは許可
return
tool_name = data.get("tool_name", "")
if tool_name != "Bash":
return # 対象外
command = data.get("tool_input", {}).get("command", "")
if not command:
return # 空コマンドは対象外
# SKIP環境変数チェック
if is_skip_env_enabled(os.environ.get(SKIP_ENV)):
log_hook_execution("my-hook", "skip", f"{SKIP_ENV}=1")
print(json.dumps({"decision": "approve"}))
return
inline_value = extract_inline_skip_env(command, SKIP_ENV)
if is_skip_env_enabled(inline_value):
log_hook_execution("my-hook", "skip", f"{SKIP_ENV}=1 (inline)")
print(json.dumps({"decision": "approve"}))
return
# メインチェック
should_block_result, reason = should_block(command)
if should_block_result:
result = make_block_result("my-hook", reason)
log_hook_execution("my-hook", "block", reason, {"command": command})
print(json.dumps(result))
return
# 対象外またはOK: 出力なし
if __name__ == "__main__":
main()
3. settings.jsonに登録
{
"hooks": {
"PreToolUse": [
{
"matcher": "Bash",
"hooks": [
{
"type": "command",
"command": "python3 $CLAUDE_PROJECT_DIR/.claude/hooks/my-hook.py"
}
]
}
]
}
}
チェックリスト
- テストを先に書く(TDD)
- 最低3つのテストケース(正常・境界・エラー)
- SKIP環境変数のサポート
- Fail-open設計(エラー時は許可)
-
log_hook_execution()でログ記録 - docstring追加(D101-D103対応)
フック統計
| イベント | フック数 |
|---|---|
| SessionStart | 5 |
| PreToolUse (Navigation) | 1 |
| PreToolUse (Edit/Write) | 3 |
| PreToolUse (Bash) | 34 |
| PostToolUse (Bash) | 17 |
| PostToolUse (Edit) | 2 |
| PostToolUse (Read/Glob/Grep) | 2 |
| PostToolUse (WebSearch/WebFetch) | 1 |
| Stop | 13 |
| 合計 | 78 |
注: ユニークフック数は75種類。一部のフック(task-start-checklist等)は複数のトリガーで発動するため、発動ポイント数(78)とは異なる。