先日、修正タスクをエージェントに任せて、そのままコミットまで進めてもらいました。差分を後から眺めると、直したかったファイルの隣に、自動整形ツールが残した .bak が一緒に入っていました。コミットメッセージは一行、ただ「Fix」とだけ。
コードそのものは正しかったのです。困ったのは、その一件をあとで追いかけたくなったときでした。
複数のアプリを一人で回している個人開発では、エージェントに手を動かしてもらう場面が増えるほど、事故はコードの中身ではなく「何をコミットに含めたか」と「何を書き残したか」の側で起きます。今日はその二つを、仕組みで淡々と抑える方法を書きます。
何が混ざるのか — git add -A が拾う作業の副産物
エージェントにコミットまで頼むと、たいてい git add -A か git add . が使われます。手軽ですが、この一手が作業ディレクトリに落ちた副産物をまとめて拾ってしまいます。
厄介なのは、副産物の多くが「そのタスクの正しい成果物のすぐ隣」に生まれることです。整形ツールの .bak、一時的なログ、ビルド出力、エディタが残す ~ 付きファイル。どれも .gitignore に書き忘れていれば、そのまま履歴に載ります。
私が実際に踏んだのは、あるスクリプトが --fix のたびに残す .bak でした。git add -A がそれを毎回拾い、リポジトリに小さなゴミが積もっていきました。混入しやすいものを一覧にしておくと、対策の的が絞れます。
| 副産物 | 典型的なパターン | 発生源 |
|---|---|---|
| バックアップ | *.bak / *.orig / *~ | 整形・置換ツール |
| ビルド出力 | dist/ / .next/ / build/ | ビルド・プレビュー |
| 依存物 | node_modules/ | パッケージ導入 |
| ログ・一時物 | *.log / .DS_Store | 実行・OS |
ステージングを許可リストで縛る
最初の一手は単純です。エージェントに -A を使わせないこと。そのタスクで触ってよい範囲をこちらから渡し、コミットはその範囲だけを明示的に add させます。
たとえば記事コンテンツだけを触るタスクなら、指示にこう書きます。「コミットは content/ 配下のみを git add content/ で行うこと。git add -A と git add . は使わないこと」。範囲を言葉で固定するだけでも、拾いすぎの大半は消えます。
ただ、言葉の約束は破られることがあります。エージェントが気を利かせて別の場所も直し、うっかり範囲外を add してしまう。だからもう一枚、機械的な網を用意します。
事故を止めるプリフライト
コミットの直前に、ステージ済みの一覧が「許可範囲の中だけか」を確かめる小さなスクリプトを挟みます。許可リストから外れるパス、または副産物のパターンに一致するパスが一つでもあれば、コミットを止めます。
#!/usr/bin/env bash
# stage-guard.sh — エージェントのコミット前に走らせる
set -euo pipefail
# 触ってよい範囲(許可リスト)。ここに含まれないパスが staged なら中断
ALLOW='^(src/|content/|docs/)'
# 混入しやすい副産物(拒否リスト)
DENY='(\.bak$|~$|\.orig$|\.log$|/node_modules/|/\.next/|/dist/|\.DS_Store$)'
staged="$(git diff --cached --name-only)"
[ -z "$staged" ] && { echo "staged なし"; exit 1; }
bad=""
while IFS= read -r f; do
if [[ "$f" =~ $DENY ]]; then bad+=" DENY $f"$'\n'; continue; fi
if [[ ! "$f" =~ $ALLOW ]]; then bad+=" OUT $f"$'\n'; fi
done <<< "$staged"
if [ -n "$bad" ]; then
echo "想定外のファイルが staged にあります:"
printf '%s' "$bad"
echo "→ git restore --staged <path> で外してから再実行してください"
exit 1
fi
echo "staged はすべて許可範囲内です"ALLOW と DENY は自分のリポジトリに合わせて書き換えてください。ポイントは、許可リストと拒否リストを両方持つことです。拒否リストは既知のゴミを弾き、許可リストは「知らないうちに増えた未知のパス」を弾きます。後者があるおかげで、新しい種類の副産物が現れても素通りしません。
このスクリプトを git commit の前段に置けば、エージェントがどんな add をしたかに関わらず、範囲外の混入はコミットの手前で止まります。作業領域そのものを分ける発想は、複数エージェントを同じリポジトリで並走させると壊れる — worktree で作業領域を分離する設計でも扱っています。あわせて読むと、範囲を絞る二つの層が見えてきます。
コミットメッセージを規約テンプレートで埋めさせる
範囲の次は、書き残しです。「Fix」だけのメッセージが困るのは、半年後の自分がその一行から何も思い出せないからです。エージェントに任せるほど、後から差分をたどる手がかりは、このメッセージだけになります。
自由記述を求めると精度がぶれます。埋めるだけのテンプレートを渡すと安定します。
cat > .gitmessage <<'MSG'
# <type>(<scope>): 何を変えたか(現在形・50字以内)
# type = add / fix / refactor / chore / docs
#
# 空行を1つ空けて、なぜそう変えたかを1〜2文で。
# 「症状」ではなく「原因と対処」を書く。
MSG
git config commit.template .gitmessageそのうえで、エージェントへの指示にこの規約を一行足します。「コミットメッセージは <type>(<scope>): 要約 の形にし、本文になぜ変えたかを必ず1文添えること」。fix(cli): 401 を再認証で解消 のように、種別・範囲・要点がそろうだけで、後日の追跡はまるで変わります。
原因と対処を残す習慣は、回帰を最短で切り分けるための土台になります。エージェントが壊す前提で運用を組む考え方は、エージェントが壊す前提で組む — ブラスト半径を小さく保つ運用設計でも述べています。
まとめ — 次にやる一つのこと
エージェントのコミットは、コードの正しさよりも「範囲」と「言葉」で事故ります。今日のうちに一つだけやるなら、stage-guard.sh を一本置いて、いつも使うタスクの指示に「-A を使わない・メッセージは規約に従う」の二文を足してください。
仕組みが一度できれば、あとはエージェントに安心して手を動かしてもらえます。私自身まだ運用を整えている途中ですが、この二枚の網を入れてから、差分を眺めるのが怖くなくなりました。お読みいただきありがとうございました。