Claude Code を個人プロジェクトで毎日触っていると、git push は 1 日に何度も走ります。そのたびに、「秘密情報が混ざっていないか」「別タブで積まれた commit を巻き込んでいないか」といった点を毎回確認したいのですが、手動でやると抜けます。私自身も、Claude に任せても、確率的に忘れます。
そこで、この確認を hook に代行させる仕組みを作っています。以下はその実装と、運用して得た手応えの話です。
何を守りたいのか
git push の前に、次の 2 つを毎回通したい、というのが出発点でした。
- 秘密情報チェック: API キー・トークン・メールアドレス・
.envの中身がコミットに混ざっていないか - commit 範囲チェック:
origin/main..HEADに、自分の担当外の commit(別タブで積まれた commit)が混ざっていないか
1 は単純な secret スキャンでも潰せます。ただ、機械スキャンでは微妙な怪しさ(「秘密ではないがプライベートな情報」)までは拾いきれません。2 は、タブを並列で動かしている以上、自分の commit 以外が HEAD に積まれている可能性が常にあります。
これらを毎回、Claude のサブエージェント(security-reviewer)に依頼して差分を読ませてから push する、という運用にしています。
ルールだけでは守れない
素直に考えると、CLAUDE.md や .claude/rules/ に「push 前に security-reviewer を呼べ」と書くだけで終わりそうです。実際、そうしていた時期もありました。
ただ、人間も AI も忘れます。作業に集中していると、push する瞬間にレビュー工程が頭から抜けます。Claude も、長いタスクの流れの中で、ルールを 100% 守るとは限りません。ルールは、読み手が読んでいる時にだけ効く仕組みです。
ルールに書くだけだと「気をつける」になります。hook に落とすと「機械的に防ぐ」になります。この違いは、実運用では大きいです。
PreToolUse hook でゲートを張る
Claude Code には PreToolUse hook という仕組みがあります。ツールが実行される直前にスクリプトを走らせて、許可・拒否を決められます。
これを使って、Bash ツールで git push が走ろうとした瞬間に「security-reviewer 経由のレビューが終わっているか」を確認し、終わっていなければ push を弾く、という仕組みを作りました。
hook の登録は .claude/settings.json の hooks.PreToolUse で行います。マッチャに Bash を指定して、実行前にガード用スクリプトを呼ぶ、という最小の設定です。
{
"hooks": {
"PreToolUse": [
{
"matcher": "Bash",
"hooks": [
{
"type": "command",
"command": "python3 .claude/hooks/git-push-guard.py"
}
]
}
]
}
}
スクリプト側は $CLAUDE_TOOL_INPUT(実行予定のコマンド)を読み、git push を含む時だけガード処理に入ります。git push でなければ何もせず通す、というゲート方式です。
「レビューが終わっている」ことを表すために、logs/security-review/ に マーカーファイル を置いています。security-reviewer がレビューを完了すると、対応する commit 範囲の hash を名前に含むマーカーを生成します。hook は、そのマーカーが存在するかを確認するだけです。
マーカーの設計で悩んだこと
ここで少し踏み込んだ話をすると、「commit 範囲の hash」の取り方にひと工夫が必要でした。
単純に「最新 commit の sha」をマーカー名にすると、次のケースで抜けます。
- 自分が commit A を作り、レビュー → marker(A) 作成
- 別タブが commit B を main に積む(自分の HEAD = A + B)
- 自分が push しようとする
- marker(A) は存在するので hook は素通し → 未レビューの B が push される
これを防ぐために、マーカー名は sha(origin/main..HEAD の commit 列全体) の range hash にしています。別タブが新しい commit を積んだ瞬間に range が変わり、マーカーが自動失効します。
range hash の計算自体は短いシェルで書けます。
RANGE_HASH=$(git log --pretty=format:%H origin/main..HEAD | sha256sum | cut -c1-16)
MARKER="logs/security-review/reviewed-${RANGE_HASH}.txt"
security-reviewer はレビュー完了時にこのマーカーを作成し、hook は push 直前に同名マーカーの存在を確認してから git push を通します。range が変われば、ここで名前が食い違って自動的に再レビューになる、という動き方です。
加えて、マーカーは single-use(1 回の push で消費される) + TTL 30 分(放置マーカーの自動クリーンアップ目的)にしています。push が通るたびにマーカーは消え、時間が経てば自動で掃除されます。
この 3 つ(range hash / single-use / TTL)を組み合わせることで、「自分が最後にレビューしたのと同じ commit 列を、1 回だけ push できる」という状態になります。
副次効果
この仕組みを入れてから、副次的に別の効果もありました。
1 つは、「別タブが積んだ commit が混ざっていないか」を毎回目視できることです。security-reviewer は diff を読むので、レビュー結果として「この range には担当外の commit が混ざっています」と教えてくれます。複数タブで並列に作業している時、これで何度か事故を防いでいます。
もう 1 つは、push の粒度が整うことです。レビューを通すコストが一定あるので、「とりあえず push」が自然に減り、意味のあるまとまりごとに push するようになりました。
何が変わったか
この仕組みを入れる前は、push のたびに「本当に大丈夫かな」という小さな不安がありました。入れた後は、その不安が hook に移譲されました。
私は忘れていい。hook が忘れない。そういう分担ができると、手元の作業が少し軽くなります。
ルールは、読む人が覚えていなければ効きません。hook は、忘れている時にこそ効きます。私にとってこの仕組みは、「ルールで自分を縛る」から「装置に守ってもらう」への移行点でした。