Tsukutta

Claude Codeのpush事故を機械で止める

― APIキー流出と誤push先ガード

Lily2026年6月18日公開
About 10 min read8

「Claude Code環境」シリーズです。前回はClaudeとCodexを1台で協業させる話を書きました。今回は、その出口 ―― AIにgitを叩かせるときに事故を機械で止めるフックの話です。

AIエージェントにcommitやpushまで任せると、便利な反面こわいのが2つあります。①うっかりAPIキーや.envを巻き込む②間違ったリポジトリにpushする。どちらも「気をつける」では防げないので、PreToolUse フックで機械的にブロックしています。

なぜEdit/Write監視だけでは足りないか

Claude Codeには「ファイル書き込み時に秘密をスキャンする」フック(Edit/Write用)はよくあります。でもそれだけだと穴が空きます。git commit git push もBashツール経由だからです。ファイルを書いた瞬間ではなく、ステージしてpushする瞬間にもう一枚ゲートが要る。そこで Bash ツールに対する PreToolUse フックを足しました。

設計:全Bashコールに乗るので、まず即return

このフックはすべてのBashコールで発火します。だから「git以外のコマンドにコストを一切かけない」ことが最優先です。冒頭で文字列に git が無ければ、Pythonを一度も起動せず即終了します。

text
# 粗い早期return: payload に 'git' が無ければ python を1度も起動せず即終了。
case "$INPUT" in
  *git*) ;;
  *) exit 0 ;;
esac
フックは「全コールに課税される」前提で書くのが大事です。旧実装はJSONパースに python3 を3回起動していて、毎コール数百msの課税になっていました。パースは1回に統合し、そもそもgitが絡まない大多数のコールは正規表現の早期returnで素通りさせます。

その後、ツールが Bash で、コマンドに git の単語境界があり、commitpush を含むときだけ本処理に入ります。

ガード1:hookバイパスを禁止する

まず塞ぐのは「フックの無効化」そのものです。--no-verify のようなバイパスフラグを含むgitコマンドはブロックします。

text
if printf '%s' "$CMD" | grep -qE -- '(--no-verify|--no-gpg-sign|commit\.gpgsign=false)'; then
  block "hook bypass フラグが含まれています。明示依頼が無い限り使用禁止。"
fi

これが無いと、エージェントが「フックに弾かれたから --no-verify を付けて再実行」という最悪の回避を学習しかねません。ゲートの前に、ゲートを外す行為自体を禁じておきます。

ガード2:commitに秘密が混ざっていないか

git commit のときは、ステージ済みdiffgit diff --cached)を走査します。ワーキングツリー全体ではなく「これからコミットされる差分」だけを見るのが肝です。

text
DIFF=$(git diff --cached 2>/dev/null || true)

# AWS Access Key の形
printf '%s' "$DIFF" | grep -qE 'AKIA[0-9A-Z]{16}' && block "AWS Access Key が含まれています"

# secret= "..." / api_key: "..." のような代入
printf '%s' "$DIFF" | grep -qE \
  '(secret|api_key|apikey|access_token|private_key|client_secret)[[:space:]]*[:=][[:space:]]*["'"'"'][A-Za-z0-9/+_=-]{20,}' \
  && block "シークレットらしき値が含まれています"

# .env がステージされている(.env.example はOK)
git diff --cached --name-only | grep -E '(^|/)\.env' | grep -qvE '\.(example|sample|template)$' \
  && block ".env ファイルがステージされています"

3つ目の .env 判定が地味に効きます。.env はブロックしつつ .env.example / .env.sample / .env.template は通す。テンプレは公開したいけど実体は絶対に出したくない、という現実に合わせています。

ガード3:pushの宛先owner検証

個人的にいちばん効いているのがこれです。push先リポジトリのownerが自分のものかを検証し、許可リストにないownerへのpushをブロックします。

text
ALLOWED_OWNERS="bokuwalily"   # 自分のGitHub owner(スペース区切りで複数可)
# ...
URL=$(git remote get-url "$REMOTE")
OWNER=$(printf '%s' "$URL" | sed -nE 's#.*github\.com[:/]+([^/]+)/.*#\1#p')
# owner が ALLOWED_OWNERS になければ block

なぜこれが要るか。OSSをforkして触っていると、リモートに他人のリポジトリが紛れ込みます。そこへ誤pushすると、自分のコードが第三者リポに飛ぶ事故になる。私はアカウント名を改名した経緯もあって、「自分の現owner以外へのpushは全部止める」を機械で担保したかった。意図的に他ownerへ出すときだけ、許可リストに足します。

運用:fail-secure と可視化

設計の原則は2つです。

  • fail-secure:判定に迷ったら通さない。ブロックは decision: block をJSONで返して exit 2。
  • 正規の抜け道を残す:許可リストやテンプレ拡張子のように、「意図的なら通る道」を用意する。完全に塞ぐと、人間が正当な操作をするたびにフックと格闘することになる。

この2枚(commit走査+push宛先検証)に、push直前の出力検証まで足したワンコマンド公開フロー(secret→remote→出力検証→push→デプロイ→疎通)を別途用意していますが、土台はこのフックです。

踏んだ落とし穴

  • 全Bashに乗るのでパース課税が痛い → gitを含まなければ即return、Pythonパースは1回に統合
  • ワーキングツリー全体を走査して誤検知git diff --cached でステージ分だけ見る
  • .env.example まで巻き込みブロック → テンプレ拡張子は除外
  • fork作業でリモートに他人リポが混入 → push先ownerを許可リストで検証
  • エージェントが --no-verify で回避 → バイパスフラグ自体をブロック

まとめ

  • commit/pushはBash経由なので、Edit/Write監視とは別に PreToolUse ゲートが要る
  • 全Bashに乗るから、gitが無ければ即returnでコストゼロ
  • commitはステージ済みdiffだけを秘密スキャン、.envはブロックしテンプレは通す
  • pushは宛先ownerを許可リスト検証して誤push・第三者リポ流出を止める
  • fail-secure+正規の抜け道。完全に塞がず、意図的操作だけ通す

次回は、毎セッションの会話ログを長期記憶に流し込むパイプラインの話 ―― 会話ログをObsidianの長期記憶に変えるを書きます。

この記事が良かったら

「チップをリクエスト」で著者にチップの受け取り設定をお願いできます

シェア