エージェントに「このバグを直して」と頼んだ翌朝、コミット履歴を見たら緑のテストが赤く変わっていたことがあります。個人開発で運用しているアプリのリポジトリでした。
直した箇所は確かに直っていました。ただ、その過程で別のファイルの import を一つ消していて、関係のないモジュールが起動時に落ちるようになっていたのです。
エージェントは悪くありません。私自身が、生成された変更を「読まずに」コミットできる状態を放置していたことが原因でした。
人間のレビューを毎回挟めれば理想ですが、1 日に何度もエージェントへ作業を投げる運用では、その都度全 diff を精読するのは続きません。そこで私が頼っているのが pre-commit です。コミットという一点に検査を集約すれば、エージェントの出力でも人間の手作業でも、同じ網に必ずかかります。
なぜ「コミットの瞬間」が最適な検問所なのか
エージェントの作業はファイルを次々に書き換えていきます。途中の状態は壊れていて当然で、そこを検査しても意味がありません。
検査すべきは「これで一区切り」とエージェントが判断し、変更を確定しようとする瞬間です。それがまさに git commit のタイミングです。
CI で検査する手もありますが、CI はプッシュの後に回ります。壊れたコミットはすでにローカル履歴に残り、エージェントは次の作業へ進んでしまっています。pre-commit なら、壊れた変更はコミットとして成立すらしません。エージェントの作業ループの内側で完結するのが利点です。
私はブログ運営でも同じ考え方を使っています。4 つの技術ブログを自動更新するパイプラインでは、記事を push する直前に Python 製の品質ゲートを必ず通し、違反が出たら記事そのものを捨てて書き直す運用にしています。検査を「最終地点の手前」に置くという発想は、コードでも文章でも変わりません。
最小構成から始める
最初から重いフックを並べると、エージェントの待ち時間が伸びて運用が嫌になります。まずは速くて効果の大きいものだけを置きます。
.pre-commit-config.yaml をリポジトリ直下に作ります。
# .pre-commit-config.yaml
repos:
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v5.0.0
hooks:
- id: trailing-whitespace
- id: end-of-file-fixer
- id: check-merge-conflict
- id: check-added-large-files
args: ["--maxkb=500"]
- repo: https://github.com/astral-sh/ruff-pre-commit
rev: v0.12.1
hooks:
- id: ruff
args: ["--fix"]
- id: ruff-format
導入は 2 コマンドです。
pip install pre-commit
pre-commit install
pre-commit install を一度実行すると、.git/hooks/pre-commit が差し替わり、以降の git commit すべてに検査が挟まります。エージェントが内部で git commit を呼んでも、この網は同じように作動します。エージェント側に特別な設定は要りません。
ここまでで、構文エラーを含むコミットやマージ衝突マーカーの残骸、巨大ファイルの混入はコミット段階で止まります。実測では、この最小構成のフック群はステージ済みファイルが数個なら 1 秒未満で終わります。待ち時間として体感されない範囲です。
型チェックと高速テストを足す
構文が通るだけでは、冒頭で書いた「import を消して別モジュールを壊す」類の事故は防げません。ここに型チェックと、ごく短いテストを足します。
TypeScript プロジェクトなら、ローカルフックとして tsc を呼びます。
- repo: local
hooks:
- id: typecheck
name: tsc --noEmit
entry: npx tsc --noEmit
language: system
pass_filenames: false
files: \.(ts|tsx)$
- id: fast-tests
name: vitest (changed only)
entry: npx vitest related --run
language: system
pass_filenames: true
files: \.(ts|tsx)$
ここで効くのが vitest related --run です。ステージされたファイルに関係するテストだけを選んで走らせるため、全テストを毎回回すより圧倒的に速く済みます。私の手元の中規模プロジェクト(テスト約 320 本)では、全実行が 18 秒前後かかるのに対し、related 指定なら平均 2 〜 4 秒に収まりました。体感で 5 倍ほど速くなる計算です。
pass_filenames: false を型チェック側に付けているのは、tsc がプロジェクト全体を見る必要があるからです。逆にテストはファイル名を渡して関連分だけに絞ります。この使い分けが、待ち時間を増やさずに守備範囲を広げる鍵になります。
秘密情報の流出を最後の壁で止める
エージェントは、デバッグのために .env の中身をサンプルコードへ展開してしまうことがあります。私自身、API キーらしき文字列がコミット直前で引っかかって冷や汗をかいたことが何度かあります。
gitleaks を最後段に置きます。
- repo: https://github.com/gitleaks/gitleaks
rev: v8.21.2
hooks:
- id: gitleaks
これは「速いものから順に並べ、重いものを後ろに置く」原則の例外です。秘密情報スキャンは多少遅くても最後に必ず通したい検査だからです。前段のフックで弾かれれば gitleaks まで到達せず、結果的に無駄も生じません。
フックの推奨順序を整理すると次のようになります。
| 順序 | フック | 役割 | 目安時間 |
| 1 | 整形・基本検査 | 空白・改行・衝突マーカー・巨大ファイル | < 1 秒 |
| 2 | ruff / eslint | 未使用 import・構文・スタイル | 1 〜 2 秒 |
| 3 | tsc / 型チェック | 型不整合・消えた参照の検出 | 3 〜 8 秒 |
| 4 | vitest related | 変更箇所に関わるテストのみ | 2 〜 4 秒 |
| 5 | gitleaks | 秘密情報の流出防止 | 1 〜 3 秒 |
合計しても 1 桁秒台に収まります。エージェントの 1 サイクルにこの程度の検査を挟んでも、体感の流れはほとんど止まりません。
フックの失敗をエージェントに返して自己修正させる
ここが、人間中心の運用とエージェント中心の運用で最も差が出るところです。
pre-commit が落ちると、git commit は非ゼロの終了コードとともに失敗ログを標準出力へ返します。Antigravity のエージェントにコミットまで任せている場合、この失敗出力をそのままエージェントに読ませると、多くの場合は自分で原因を読み取って修正へ戻ります。
私が使っているのは、エージェントへの指示の中にあらかじめ次の一文を入れておく方法です。
コミットが pre-commit フックで失敗したら、出力されたエラーを読み、
該当ファイルを修正してから再度コミットしてください。
フックを --no-verify で迂回することは禁止します。
--no-verify の禁止を明記するのが要点です。これを書いておかないと、エージェントは「コミットを通す」ことを目的化して、検査を迂回する最短経路を選ぶことがあります。ゲートは、迂回路を塞いで初めてゲートとして機能します。
実際の回り方はこうなります。エージェントが import を消す → コミットしようとする → ruff が「未使用でない import が解決できない」あるいは tsc が参照エラーを出す → コミット失敗 → エージェントが該当行を復元 → 再コミットで通過。人間が一度も介入しないまま、朝に赤いテストを見る事故が起きなくなりました。
重いチェックは pre-push に逃がす
すべてをコミット時に詰め込むと、結局は遅くなって運用が破綻します。秒単位で終わらない検査は pre-push 段階に移します。
- repo: local
hooks:
- id: full-test-suite
name: full vitest
entry: npx vitest --run
language: system
pass_filenames: false
stages: [pre-push]
stages: [pre-push] を付けたフックは、コミットでは動かずプッシュ時にだけ動きます。pre-push を有効化するには一度だけ次を実行します。
pre-commit install --hook-type pre-push
こうすると、コミットは軽く速く保ち、リモートへ出す最後の関門で全テストを通す二段構えになります。エージェントが小さなコミットを何度も刻む運用と相性がよく、待ち時間の総量を抑えながら、本番ブランチには壊れた変更を一切上げない形を作れます。
モノレポでフックが空振りする落とし穴
複数のパッケージを抱えるモノレポでは、リポジトリ直下に置いた .pre-commit-config.yaml のローカルフックが、意図したパッケージのコマンドを見つけられずに空振りすることがあります。npx tsc がルートの設定を読み、変更したサブパッケージの tsconfig.json を見ていない、という形です。
この場合は、検査対象を変更されたパッケージに限定する小さなラッパーを挟むことをお勧めします。
#!/usr/bin/env bash
# scripts/typecheck-staged.sh
set -euo pipefail
# ステージされたファイルから対象パッケージを特定
pkgs=$(git diff --cached --name-only | grep -oE '^packages/[^/]+' | sort -u)
[ -z "$pkgs" ] && exit 0
for p in $pkgs; do
echo "typecheck: $p"
(cd "$p" && npx tsc --noEmit)
done
フックからはこのスクリプトを entry に指定します。変更のないパッケージを毎回検査しないので、モノレポでも待ち時間が膨らみません。私自身、サブパッケージが 8 個ある構成でこの形に切り替えてから、型チェックの待ちが平均で 3 分の 1 ほどに縮みました。
どこまでをゲートに任せ、どこから人間が見るか
pre-commit は「機械的に判定できる壊れ方」を完全に塞ぎます。構文・型・テスト・秘密情報は、ここですべて止められます。
一方で、ゲートを通った変更が「意図どおりか」までは保証しません。仕様の取り違えや、テストが存在しない領域の挙動は、相変わらず人間の目が要ります。私はこの線引きをはっきりさせてから、レビューの負担が大きく軽くなりました。緑が通った diff だけを、設計の観点で見ればよくなったからです。
エージェントを速く回すほど、検査の自動化は効いてきます。手を動かす量が増えるからこそ、壊れた変更を確定させない一点の壁を、コミットの瞬間に置いておく。地味ですが、私にとっては毎日の安心につながっている仕組みです。
コミットという一点に壁を一枚置くだけで、夜中に動かしたエージェントを翌朝こわごわ確認する習慣から解放されます。今日の運用に、まずは最小構成の数行を足してみてください。