Edit/Writeを見張っていれば安心だと思っていました
「Claude Code環境」シリーズです。前回はClaudeとCodexを1台で協業させる話を書きました。今回はその出口の話です。AIにgitを叩かせるとき、事故を機械で止めるフックを作りました。
AIにcommitやpushまで任せると、こわいことが2つあります。
- うっかりAPIキーや.envを巻き込む
- 間違ったリポジトリにpushする
どちらも「気をつける」では防げません。なのでPreToolUseフックで機械的にブロックしています。
最初はEdit/Write用の秘密スキャンだけ入れていました。ファイルを書いた瞬間に、APIキーっぽい文字列を見つけて止めるやつです。これでもう漏れないと思っていました。
ところがある日、ログを何気なく眺めていて手が止まりました。AIがターミナル経由でファイルをまとめてcommitしている行が、普通に流れていたんです。あれ、Edit/Writeのフック、この間一回も発火してない!
git commitもgit pushも、Bashツールの中で完結します。ファイルの中身を見張っていても、git操作は別の入口から素通りしていました。しかもエラーも警告も出ません。ちゃんと動いているように見えて、一番危ない扉が開いていたわけです。
非エンジニアの自分には、この「入口が複数ある」という発想がまるでありませんでした。ログを見返す習慣がなかったら、今も気づいていないと思います。そこでBashツールに対するPreToolUseフックを足しました。
💬 集客・導線設計と、AI導入・自動化のご相談はこちら
lsまで遅い!犯人は自分のフックでした
このフックはすべてのBashコールで発火します。lsでもcatでも走ります。
旧実装のころ、Claude Codeの作業全体がじわっと遅くなった時期がありました。gitと関係ない軽いコマンドまで、妙にワンテンポ遅れるんです。最初は回線かPCのせいだと思っていました。
フックのログを追ってみたら、毎回のBashコールでpython3が3回起動していました。JSONのパースのためだけに、毎コール数百msずつ払っていたんです。便利機能のつもりで仕込んだものが、体感速度を静かに削っていました。
直し方はシンプルです。入力にgitという文字列がなければ、Pythonを一度も起動せずに即終了します。
# 粗い早期return: payload に 'git' が無ければ python を1度も起動せず即終了。
case "$INPUT" in
*git*) ;;
*) exit 0 ;;
esacパースも1回にまとめました。gitが絡まない大多数のコールは、ここで素通りします。
それ以来、新しいフックを足すたびに「これ、gitと無関係なコマンドでも走るよね?」と自分に聞いています。作業のテンポが悪いと感じたら、最近自分が足した仕組みをまず疑うようになりました。
ガード1:フックを外す行為そのものを禁止
最初に塞いだのは、フックの無効化です。--no-verifyのようなバイパスフラグを含むgitコマンドはブロックします。
if printf '%s' "$CMD" | grep -qE -- '(--no-verify|--no-gpg-sign|commit\.gpgsign=false)'; then
block "hook bypass フラグが含まれています。明示依頼が無い限り使用禁止。"
fiきっかけは、ブロックされた直後のAIの動きです。「止められた、じゃあ別の方法で」と代替手段を試そうとする瞬間を何度か見ました。人間なら怒られたら素直に直します。AIは律儀に別ルートを探しにいくことがあります。
放っておくと「--no-verifyを付けて再実行」という最悪の回避を覚えかねません。なので、探しにいく入口ごと消しました。
ガード2:commitの中身に秘密が混ざっていないか
git commitのときは、ステージ済みのdiff(git 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は通します。テンプレは公開したい、実体は絶対に出したくない。この現実に合わせました。
一番手こずったのは、正規表現の「20文字以上」という線です。最初はもっと緩くしていました。すると、ドキュメント用に書いた api_key: "your-key-here" のようなプレースホルダまで止まります。説明を書きたいだけなのに、何度もブロックされました笑
かといって緩めすぎると、本物のキーを見逃します。止まるたびに「これは本物?またプレースホルダ?」と一つずつ確かめました。最終的に「それっぽい長さのランダム文字列か」で線を引き、短いプレースホルダは通すようにしています。誤検知を実際に踏んでから調整するしかない作業でした。
ガード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 になければ blockOSSをforkして触っていると、リモートに他人のリポジトリが紛れ込みます。そこへ誤pushすると、自分のコードが第三者のリポに飛びます。
入れる前に一度ヒヤッとしました。forkしたリポで作業中、リモートの向き先を確認せずに git push とだけ打ったんです。Enterを押した直後に「これ、upstreamに飛んでない?」と冷や汗が出ました。そのときは無事でしたが、一瞬でも不安になる時点で仕組みが足りていません。
私はアカウント名を改名した経緯もあります。なので「今のowner以外へのpushは全部止める」を機械で担保したかったんです。意図的に他のownerへ出すときだけ、許可リストに足します。
毎回リモートのURLを目で確認する運用は、どこかで必ずサボります。コードの中身は読めなくても、「宛先が違えば通らない」という条件は完全に理解できました。非エンジニアの自分が一番安心できたのは、実はこのガードです。
迷ったら止める、でも抜け道は残す
設計の原則は2つです。
- 判定に迷ったら通さない(fail-secure)。ブロック時は decision: block をJSONで返してexit 2
- 意図的な操作だけ通る道を残す。許可リストやテンプレ拡張子がそれにあたる
完全に塞ぐと、正当な操作のたびにフックと格闘することになります。逆にfail-secureだと、たまに「本当は問題ないのに止まった」が起きます。それでも、迷うくらいなら止めて人の目を挟む方を選びました。事故ってから気づくより、都度確認する方が結局早いからです。
何がブロックされたかはログに残しています。理由が追えないと、AIが「なぜ止まったか分からないまま再実行する」に流れやすいからです。
この2枚(commit走査とpush宛先検証)の上に、ワンコマンドの公開フローも別に用意しています。secret確認、remote確認、出力検証、push、デプロイ、疎通確認の順です。その一番下で支えているのが、今回のフックです。
踏んだ落とし穴と、その後どうしたか
- 全Bashに乗るのでパースの時間が痛い → gitを含まなければ即return、Pythonパースは1回に統合
- ワーキングツリー全体を走査して誤検知 → git diff --cachedでステージ分だけ見る
- .env.exampleまで巻き込んでブロック → テンプレ拡張子は除外
- fork作業でリモートに他人のリポが混入 → push先ownerを許可リストで検証
- エージェントが--no-verifyで回避しようとする → バイパスフラグ自体をブロック
並べるときれいな設計手順に見えます。でも実際は、ヒヤッとするたびに一枚ずつ足していきました。AIに実装させながら「こういう穴もあるよ」と教えてもらい、初めて気づいたガードもあります。落とし穴を踏んだ回数だけ、ガードが増えてきました。
まとめ
- commit/pushはBash経由なので、Edit/Write監視とは別にPreToolUseのゲートが要る
- 全Bashに乗るから、gitが無ければ即returnして余計な時間をかけない
- commitはステージ済みdiffだけを秘密スキャンし、.envは止めてテンプレは通す
- pushは宛先ownerを許可リストで検証し、誤pushと第三者リポへの流出を止める
- 迷ったら止める。ただし意図的な操作の抜け道は残す
これから始める人へ
コードはほぼ書かずに、AIに実装を任せています。だから「git操作までAIに任せて大丈夫か」という不安は、正直今もゼロではありません。
でも、その不安を「気をつける」で片付けようとすると、どこかで必ず抜けます。抜けそうな場所を一つずつ機械で塞ぐ方が、私には現実的でした。
最初から全部を見通す必要はありません。ヒヤッとした瞬間ごとにガードを一枚足せば、気づけば結構な数が積み上がっています。そのくらいの気楽さで始めてみてください。次回も、この延長にある運用まわりの話を書く予定です。
📕 この記事の内容を、実装まで全部まとめた有料記事があります 手順・コード・テンプレをそのまま渡しています AIで「寝てても回る仕組み」をゼロから30日で組む全手順
コメント
ログイン か 会員登録 するとコメントできます