0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Claude Codeのpush事故を機械で止める ― APIキー流出と誤push先ガード

0
Last updated at Posted at 2026-06-22

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

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

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

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

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

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

# 粗い早期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コマンドはブロックします。

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)を走査します。ワーキングツリー全体ではなく「これからコミットされる差分」だけを見るのが肝です。

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をブロックします。

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の長期記憶に変えるを書きます。


Lily@bokuwalily)― 個人開発者。Claude Code で自動化基盤を組みながら、iOSアプリやWebサービスを量産しています

皆さんの ❤️ やシェアが励みになります!

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?