Claude Codeを個人プロジェクトで毎日使っていると、git push は1日に何度も走る。そのたびに秘密情報の混入を確かめ、別タブで積まれたcommitを巻き込んでいないか確認したい。だが、手動の確認は抜ける。自分でも忘れるし、Claudeに任せても忘れる。

そこで、レビューが済んでいなければpushを止めるhookを作った。止めるだけなら単純だが、「さっきレビューした差分」と「いまpushする差分」が同じかを確かめる必要があった。複数タブで作業していると、レビュー後にもHEADが変わるからだ。

以下は、2026年4月19日時点の個人環境で使っていた構成と、その運用記録である。

レビューで確かめる二つのこと

push前に確認したいのは、秘密情報とcommitの範囲だ。

  1. APIキー、トークン、メールアドレス、.env の中身がcommitに混ざっていないか
  2. origin/main..HEAD に、自分の担当外のcommitが含まれていないか

秘密情報の一部は機械スキャンで検出できる。ただ、秘密ではなくても公開したくない私的な情報まで、パターンだけで拾い切るのは難しい。commitの範囲も、並行作業する別タブがHEADに変更を積むなら、push直前の状態を見る必要がある。

このため、Claudeのサブエージェントであるsecurity-reviewerに差分を読ませ、その結果を確認してからpushする運用にしていた。

指示ファイルから実行前のチェックへ

最初はCLAUDE.mdや .claude/rules/ に、push前にsecurity-reviewerを呼ぶよう書いていた。しかし、長いタスクに集中していると、その工程が抜ける。ルールを読んでいても、実行時に適用されるとは限らなかった。

Claude CodeのPreToolUse hookは、ツールの実行直前にスクリプトを走らせ、許可・拒否を判定する仕組みだ。これを使い、Bash ツールで git push が実行される前に、レビュー済みかを確認することにした。

.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 を含む場合だけチェックする。それ以外のコマンドは、そのまま通す。

レビュー済みであることは、logs/security-review/ のマーカーファイルで記録する。security-reviewerがレビューを終えると、対象commit範囲のhashを名前に含むマーカーを作る。hookはpush時点の範囲に対応するマーカーがあるかを調べ、なければ拒否する。

レビュー後にcommitが増えた場合

マーカーには、どの差分をレビューしたかを結びつけなければならない。レビュー済みcommitの記録があるだけでは、後から加わった変更は分からない。

例えば、次の順で作業が進む。

  1. 自分がcommit Aを作り、レビューを終えてmarker(A)を作る
  2. 別タブがcommit Bをmainへ積み、HEADが進む
  3. 自分がpushしようとする

ここでAのマーカーが存在することしか確認しなければ、未レビューのBまでpushしてしまう。実行時点の対象との照合が必要だ。

自分の実装では、origin/main..HEAD に含まれるcommit列全体をhash化した range hash を使った。別タブがcommitを追加すると列が変わり、レビュー時とは異なる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直前の範囲から同じ名前を計算する。範囲が変わればマーカーが見つからず、再レビューが必要になる。この仕組みは、当時の運用でpush対象として扱っていた origin/main..HEAD に対応している。

さらに、マーカーはsingle-use(1回のpushで消費)とし、放置した古いマーカーを片づけるためにTTLを30分にした。range hashで対象を結びつけ、single-useで再利用を防ぎ、TTLで古い記録を残さない。それぞれ役割が違う。

並行作業で得られた手応え

運用してみると、レビュー忘れを止める以外にも効果があった。security-reviewerに差分を読ませるたび、「この範囲には担当外のcommitが混ざっている」と気づける。複数タブで作業する中で、これによって何度か事故を防いだ。

pushの粒度も変わった。レビューを通す手間があるため、ひとまずpushする回数が減り、意味のある変更のまとまりごとに送るようになった。

この運用では、レビューを終えた印としてマーカーを作り、hookはその存在と対象範囲を確認する。ファイルがあることだけで、レビューが実際に行われたか、その内容が適切かまで確かめられるわけではない。それでも、レビュー完了時に記録を残す運用と組み合わせることで、覚えているかどうかに左右されていた工程の抜けを、pushの直前で止められるようになった。

以前は、pushのたびに「確認を忘れていないか」という小さな不安があった。導入後は、レビュー済み記録の確認をhookに任せ、差分の内容に注意を向けられるようになった。自分にとっては、ルールを覚え続ける負担を減らすための、具体的な装置になった。