AIエージェント 2026年8月2日

AIエージェントに自律でgit操作させたら、隣のリポジトリを汚した話——作業ディレクトリ分離とコミット前ガードの設計

XECIN Claude作業ディレクトリ分離Git技術検証

きっかけは、朝いちばんの棚卸しでした。AIワーカーを3台まで並列で走らせる運用に切り替えてしばらく経った頃、タスクを配る側——オーケストレーターのリポジトリに、誰も書いた覚えのないコミットが2つ積まれているのを見つけたんです。

中身を見たら、あるチケットの成果物であるはずのファイルでした。本来は対象プロジェクトのリポジトリに入るべきものが、まるごと親のほうへ吸い込まれていた。正直なところ、最初は自分の操作ミスを疑いました。でも履歴を追っていくと、犯人は自律で動いていたエージェント自身だった、というオチです。

今思えば、起こるべくして起きた事故でした。

私たちの運用は、1タスク=1チケットで、エージェントがコードを書き、コミットし、PRまで作る、という形です。問題は「どこで」作業させるかを最初きちんと設計していなかったこと。全ワーカーが同じ親ディレクトリをカレントにしたまま、git clone した先へ cd して作業する——くらいの雑な作りだったんですね。

これで何が起きるか。Gitのブランチの先端、つまりHEADは、プロセスごとに持っている状態ではなく、作業ディレクトリに紐づく状態です。ここを取り違えると、エージェントAがcheckoutした瞬間に、同じ場所で作業していたエージェントBの足元のブランチが変わってしまう。しかもエージェントは「今どのリポジトリの、どのブランチにいるか」を平気で忘れます。人間ならプロンプトの見た目でなんとなく気づくところを、彼らはためらいなく git add . して git commit してしまうんです。

最初のダメだったやり方

最初のアプローチは、こんな雰囲気でした。

# 最初のダメな作り: 全ワーカーが同じ場所から作業していた
cd /work
git clone "$REPO_URL" repo && cd repo   # 3台が同時にやると repo/ を奪い合う
# ...エージェントがファイルを編集...
git add .
git commit -m "作業成果"                 # どの .git に入るかはカレント次第

このスクリプトの何が怖いって、cd repo に失敗しても後続がそのまま走ることです。クローンが競合して repo/ が中途半端な状態になり、cd が滑ってカレントが /work のまま git add . が走る。/work の親をたどっていくと、たまたまオーケストレーターのリポジトリの .git があった——これが吸い込みの正体でした。

エッジケースの話になりますが(つい脱線しそうになります)、もうひとつ肝を冷やしたのが git restorecheckout です。あるワーカーが未コミットの変更を抱えたまま別のブランチへ移ろうとして、Gitに拒否されると、今度は「エラーが出たので強制フラグを付けて再実行します」を平然とやる。コミットしていない作業ディレクトリの変更は、Gitのゴミ箱(reflogやstash)にも残りません。消えたら復旧は基本的に無理です。ここは本当に危ないと感じました。

作り直した方針:エージェントの善意に依存しない

そこで作り直しました。方針はシンプルで、「エージェントの善意に依存しない」。大きく3つに分けて考えています。

1. チケット単位で作業場所を物理的に分ける

まず、作業ディレクトリをチケット単位で完全に分けました。repos/{プロジェクト}/{チケット番号}/ という固定の置き場を決めて、1チケット1ディレクトリ。並列で3台走っても、それぞれ別の物理ディレクトリなので、HEADもインデックスも衝突しません。

同一リポジトリの複数ブランチを同時にいじる場合は、クローンを増やすより git worktree のほうが軽くて好みでした。

# チケット単位に作業ツリーを分ける(同一リポジトリを並列で触る場合)
base="repos/${PROJECT}/${TICKET}"
git -C "$MAIN_CLONE" worktree add "$base" -b "ai/${TICKET}-work"

worktreeを選んだ理由は、オブジェクトDBを共有しつつ作業ツリーとHEADだけを独立させられるからです。逆に、まるごとcloneを毎回作る案は、ディスクとクローン時間がチケット数ぶん膨らむので選びませんでした。ここは好みが分かれるところですが、並列度が上がるほどworktreeが効いてきます。

2. コミットの直前に「今どこにいるか」を検証する

物理分離をしても、エージェントが cd をミスる可能性はゼロにはできません。だからコミットの直前に、いま自分がいるリポジトリのルートを検証する関門を1枚挟みました。

# コミット前ガード: 想定した作業ディレクトリのルートでなければ中断する
expected="$(cd "repos/${PROJECT}/${TICKET}" && pwd)"
root="$(git rev-parse --show-toplevel 2>/dev/null)"
if [ "$root" != "$expected" ]; then
  echo "想定外のリポジトリで作業しています: $root" >&2
  echo "オーケストレーター等への誤コミットを防ぐため停止します" >&2
  exit 1
fi
git add -A && git commit -m "[#${TICKET}] ${SUMMARY}"

これを入れてから、誤コミットは一度も起きていません。ポイントは、ルートが期待値と1文字でも違えばコミットを通さないこと。オーケストレーターのリポジトリだった場合はもちろん、そもそも .git が見つからず rev-parse が空を返す場合も止まります。

補足すると、.git が無い場所で git addcommit を走らせると、Gitはカレントから上へ上へと .git を探しにいって、見つかった親リポジトリを勝手に対象にします。これが最初の吸い込み事故の直接原因でした。上のガードは、rev-parse が親の .git を拾ってしまうケースも「期待値と違う」で弾けるので、二重に効いてくれます。

3. ガードは環境側で強制する

最後に、いちばん大事だと思っている話です。ここまでのガードを「エージェントへの指示(プロンプト)」として書くだけでは足りません。彼らは指示を要約し、都合よく解釈し、ときどき忘れます。だから効かせたいガードは、エージェントが飛び越えられない場所——実行環境のフックや権限ルール——に置くことにしました。

具体的には、危ない操作(強制付きのcheckout、reset --hard、保護パスへの書き込み)を実行前フックで止め、.git ディレクトリそのものや設定ファイルは書き込み対象から外す。組織のワーカー全体に同じルールを配って、個々のエージェントが勝手に許可を緩められないようにしています。

どこで何を守るかを、いちど表に落として整理しました。

事故パターン起きる工程直接の原因効かせたガード
隣のリポジトリへ誤コミットcommit.git 探索が親をたどる / cd 失敗コミット前の rev-parse ルート検証
並列ワーカーのHEAD衝突checkout / commit同一作業ツリーの共有チケット単位の物理分離 / worktree
未コミット変更の消失checkout / restore / reset強制フラグの安易な使用破壊的コマンドを実行前フックで停止
設定ファイルの改変任意保護パスへの書き込み.git・設定を書き込み対象外にする

実行前フックは、今のところこのくらいの素朴なものです。

# 実行前フック(簡易版): 破壊的なgitコマンドを門前払いする
cmd="$*"
case "$cmd" in
  *"checkout -f"*|*"reset --hard"*|*"clean -fd"*|*"push --force"*)
    echo "破壊的な操作のため人間の確認が必要です: $cmd" >&2
    exit 1 ;;
esac

正直、ここは改善の余地が大きいところです。文字列マッチなので、エイリアスを噛ませたり -f を離して書かれたりすると素通りしますし、本当に必要なforceを人間が承認するフローもまだ手運用のままです。将来的には引数を構造として解釈する方式に寄せたいと考えていますが、まずは「事故る操作の8割を止める」を優先しました。

数字で見た効果

数字で言うと、この分離とガードを入れる前は、作業事故(誤コミット・作業消失・HEAD衝突)が月に2〜3件のペースで起きていて、そのたびに人間が履歴を戻す作業に30分から1時間取られていました。入れたあとは、少なくとも誤コミットと吸い込みはゼロ。並列3台でも取り違えは起きていません。

振り返って

振り返って思うのは、自律エージェントを疑うのではなく、疑わなくていい環境を先に作るべきだった、ということです。彼らは驚くほど有能な反面、「今どこにいるか」みたいな文脈をあっさり手放します。そこを人間の注意力やプロンプトの但し書きで埋めようとすると、いつか必ず抜ける。

今後は、実行前フックを文字列マッチから引数解釈ベースに作り替えて、force系の承認フローも自動化したいと思っています。あと、worktreeのライフサイクル——作りっぱなしの作業ツリーが溜まっていく問題——も、そろそろちゃんと片付けないといけないな、と。事故は減らせましたが、運用の掃除のほうはまだこれからだったりします。