開いたPRに「LGTM」とだけ書かれたレビューが返ってくると、少し悲しい気持ちになります。忙しい開発現場で一つひとつのコミットを丁寧にレビューするのは現実的に難しい。そのジレンマを解消するために、私は Claude Code のフック機能を使って「コミットするたびにコードが自動でレビューされる」仕組みを作りました。
使ってみて気づいたのは、フックが単なる「コマンド実行の仕組み」ではないということです。PreToolUse・PostToolUse・Stop というイベントをうまく組み合わせると、Claude Code 自体が作業するたびにチェックを挟み込めます。CI/CD とは別の、もっとインタラクティブな品質保証の層が生まれます。
フックが動くタイミングを正確に把握する
Claude Code のフックは .claude/settings.json に定義します。フックが実行できるタイミングは4種類あります。
PreToolUse: ツール実行前(exit 2を返すとツールの実行を止められる)PostToolUse: ツール実行後(結果を受け取ってから追加処理を走らせる)Notification: Claude から通知が来たときStop: セッション終了時
この4つのうち、今回メインで使うのは PostToolUse です。Bash ツールが実行されるたびに、変更があったファイルを検出してレビューを走らせます。
重要なのは「どのツールに対してフックを仕掛けるか」を matcher で絞り込むことです。全ツールに対して重い処理をかけると、Claude Code の応答がかなり遅くなります。
{
"hooks": {
"PostToolUse": [
{
"matcher": "Bash",
"hooks": [
{
"type": "command",
"command": "python3 ~/.claude/hooks/auto-review.py"
}
]
}
]
}
}レビュースクリプトの実装
フックから呼ばれる auto-review.py の中身です。Claude Code はフックに対して標準入力でコンテキストを渡してくれるので、それを使って直前の操作内容を把握します。
#!/usr/bin/env python3
import json
import sys
import subprocess
def get_changed_files():
result = subprocess.run(
["git", "diff", "--name-only", "HEAD"],
capture_output=True, text=True
)
files = result.stdout.strip().split("\n")
return [f for f in files if f.endswith((".ts", ".tsx", ".py", ".go"))]
def review_with_claude(code: str, file_path: str):
prompt = f"""以下のコードを簡潔にレビューしてください。
問題がある場合は指摘し、問題がなければ「OK」とだけ返してください。
ファイル: {file_path}{code[:2000]}
result = subprocess.run(
["claude", "-p", prompt, "--output-format", "text"],
capture_output=True, text=True, timeout=30
)
if result.returncode == 0:
print(result.stdout)
def main():
try:
hook_input = json.loads(sys.stdin.read())
except json.JSONDecodeError:
return
tool_name = hook_input.get("tool_name", "")
if tool_name != "Bash":
return
changed_files = get_changed_files()
if not changed_files:
return
for file_path in changed_files[:3]: # 最大3ファイルまで
try:
with open(file_path) as f:
code = f.read()
except (FileNotFoundError, PermissionError):
continue
print(f"\n📋 Auto-review: {file_path}")
review_with_claude(code, file_path)
if __name__ == "__main__":
main()実際に動かすと、Bash ツールが実行されるたびにターミナルにレビューコメントが流れてきます。最初は「うるさいかも」と思いましたが、慣れると自分の癖を指摘されるのが助かります。変数名が雑になりがちな箇所や、エラーハンドリングが甘い部分をその場で気づけるようになりました。
Stop フックで「作業終了レポート」を自動生成する
もうひとつ使いやすいのが Stop フックです。セッションが終わるタイミングで自動的に「今日の作業まとめ」を生成できます。
{
"hooks": {
"Stop": [
{
"hooks": [
{
"type": "command",
"command": "python3 ~/.claude/hooks/session-report.py"
}
]
}
]
}
}session-report.py では git log --since="6 hours ago" --oneline で直近のコミットを集め、Claude CLI に渡して自然言語の要約を生成します。~/work-logs/ に日付付きで保存しておくと、週次レポートや振り返りのときに非常に役立ちます。
#!/usr/bin/env python3
import subprocess
from datetime import datetime
from pathlib import Path
def get_recent_commits():
result = subprocess.run(
["git", "log", "--since=6 hours ago", "--oneline", "--no-merges"],
capture_output=True, text=True
)
return result.stdout.strip()
def generate_report(commits: str) -> str:
if not commits:
return "コミットなし"
prompt = f"""以下のコミットログから、今日の作業内容を3行でまとめてください。
技術的な内容を平易な言葉で説明してください。
コミット:
{commits}"""
result = subprocess.run(
["claude", "-p", prompt, "--output-format", "text"],
capture_output=True, text=True, timeout=30
)
return result.stdout if result.returncode == 0 else "レポート生成失敗"
def main():
commits = get_recent_commits()
report = generate_report(commits)
log_dir = Path.home() / "work-logs"
log_dir.mkdir(exist_ok=True)
today = datetime.now().strftime("%Y-%m-%d")
log_file = log_dir / f"{today}.md"
with open(log_file, "a") as f:
timestamp = datetime.now().strftime("%H:%M")
f.write(f"\n## {timestamp} セッション終了\n\n{report}\n")
print(f"📝 作業レポートを保存しました: {log_file}")
if __name__ == "__main__":
main()PreToolUse フックで実行ブロックする際の注意点
PreToolUse フックで exit 2 を返すと、Claude Code は該当ツールの実行を止められます。ただし、実装を間違えると Claude Code がループに入る場合があります。
たとえば「TypeScript のコンパイルエラーが出たら Bash ツールの実行をブロック」するフックを作ったとき、Claude がエラー修正のために Bash ツールを使おうとして、またブロックされ、修正できない…という状況が起きました。
ブロック系のフックを作るときの設計原則は「Claude がリカバリーするために使うツールはブロックしない」です。具体的には matcher に正規表現を使い、危険なコマンドのパターンだけを対象にするのが安全です。
{
"hooks": {
"PreToolUse": [
{
"matcher": "Bash",
"hooks": [
{
"type": "command",
"command": "python3 ~/.claude/hooks/safety-check.py"
}
]
}
]
}
}#!/usr/bin/env python3
import json
import sys
import re
DANGEROUS_PATTERNS = [
r"rm\s+-rf\s+/",
r"DROP\s+TABLE",
r"truncate\s+/",
]
def main():
try:
hook_input = json.loads(sys.stdin.read())
except json.JSONDecodeError:
sys.exit(0)
command = hook_input.get("tool_input", {}).get("command", "")
for pattern in DANGEROUS_PATTERNS:
if re.search(pattern, command, re.IGNORECASE):
print(f"⚠️ 危険なコマンドを検出しました: {command[:100]}")
sys.exit(2) # exit 2 でツール実行をブロック
if __name__ == "__main__":
main()exit 2 を返したとき、Claude Code には「ブロックした理由」が標準出力経由で伝わります。適切なメッセージを出力しておくと、Claude が次の行動を判断しやすくなります。
フックを有効化する手順
フックはユーザースコープとプロジェクトスコープの2箇所に設定できます。
- ユーザースコープ:
~/.claude/settings.json— 全プロジェクト共通 - プロジェクトスコープ:
.claude/settings.json— そのプロジェクトのみ
今回紹介したレビューフックは汎用的なので、ユーザースコープに置くのがおすすめです。~/.claude/hooks/ ディレクトリを作り、スクリプトを配置してから settings.json に追記するだけで使い始められます。
mkdir -p ~/.claude/hooks
# スクリプトを配置
chmod +x ~/.claude/hooks/auto-review.py
chmod +x ~/.claude/hooks/session-report.py3週間使って見えてきたこと
フックを使い始めて3週間ほど経ちましたが、コードの品質が上がったかどうかは正直まだ測れていません。ただ、「後回し癖」はかなり改善しました。普段なら「後でリファクタリングしよう」と放置していた箇所が、その場で指摘されるので直すようになりました。
Claude Code のフック機能は、まだ活用している人が少ない領域です。公式ドキュメントには書いてありますが、どう組み合わせるかは自分で考えるしかありません。それが今は試行錯誤の余地として楽しくもあります。
まずは Stop フックだけ追加して、セッション終了時のログ出力から始めてみてください。リスクがなく、すぐに価値を実感できます。