朝、夜間に走らせていた定期実行のログを確認すると、エージェントは「コミットして push しました」と報告していました。終了コードも 0。それなのに GitHub のリポジトリを開くと、最新コミットは前日のまま。エラーは一行も出ていません。
個人開発のいくつかのプロジェクトで、リポジトリへの定期的なデータ更新を AI エージェントに任せています。その運用を始めてから私自身が最初に深くハマったのが、この「無言の失敗」でした。エラーで止まってくれる障害よりも、成功した顔をして何もしていない障害のほうがずっと厄介です。気づくまでに丸一日かかりました。
原因をたどると、git のごく基本的な仕様に行き着きます。ただ、自動化環境特有の条件が重なると、その仕様が人間の目から完全に隠れてしまうのです。
症状 — ログは正常、リモートは無変化
観測できた事実は次の3点です。
- エージェントの実行ログは正常終了(exit code 0)
git push origin mainの出力はEverything up-to-date- リモートの main ブランチに新しいコミットが存在しない
Everything up-to-date は一見すると成功のメッセージに見えます。実際には「push すべきコミットが何もなかった」という意味で、ここが見落としの入り口になります。
なお、認証情報の問題で Permission denied が返るケースは別系統です。そちらの切り分けは Antigravity の Agent から git push したら permission denied になるときの切り分けと恒久対策 にまとめています。今回扱うのは、エラーが一切出ないまま空振りするケースです。
再現条件 — 使い捨て環境とエラーの握りつぶし
この問題は、次の3条件が揃うと再現します。
- 使い捨ての VM やコンテナで
git clone直後に作業している(グローバルの.gitconfigが存在しない) - スクリプトが
set -eなしで動いている、またはコミット行が|| trueなどで保護されている - 実行ログを人間がリアルタイムで見ていない
clone したばかりの環境には user.name も user.email もありません。この状態で git commit を実行すると、git は「Please tell me who you are」というメッセージとともにコミットを拒否します。
対話的なターミナルならすぐ気づきます。しかし自動実行では、このメッセージはログの中ほどに埋もれ、スクリプトはそのまま次の行へ進みます。コミットされていないので push 対象はゼロ。git push は「送るものがない」状態を正常として終了コード 0 で終わります。
GitHub Actions などの CI では、テンプレートや既製のアクションが identity を設定してくれることが多く、この躓きを経験しないまま自動化を組んでいる方も多いはずです。素のコンテナでエージェントを動かす場合、その便利な層は存在しません。
原因 — commit の無言失敗と push の仕様の組み合わせ
整理すると、原因は2つの仕様の重なりです。
ひとつは、git が identity(user.name / user.email)未設定のコミットを拒否すること。これ自体は正しい挙動です。
もうひとつは、git push が「push すべきものがない」場合をエラーにしないこと。Everything up-to-date は失敗ではなく「仕事がなかった」という報告であり、終了コードは 0 です。
つまり「commit が静かに失敗 → push が空振りで成功 → エージェントは成功と報告」という連鎖です。どのコマンドも仕様どおりに動いていて、壊れている箇所はどこにもありません。だからこそ気づきにくいのだと思います。エージェントの判断基準が「コマンドの終了コードが 0 だったか」だけだと、この連鎖は完全な成功に見えてしまいます。
対処1 — identity の明示と「SHA 照合」での成功判定
まず identity の明示です。クローン直後に、リポジトリローカルで必ず設定します。
cd /path/to/repo
git config user.email "bot@example.com"
git config user.name "Update Bot"ワンショットで済ませたい場合は -c オプションで都度渡す方法もあります。
git -c user.email="bot@example.com" -c user.name="Update Bot" \
commit -m "Update data files"そして、より本質的な対処が成功判定の変更です。私はこの一件以来、push の成否を終了コードではなく「ローカルとリモートの先端 SHA が一致したか」で判定するようにしました。
#!/bin/bash
set -euo pipefail
cd /path/to/repo
git config user.email "bot@example.com"
git config user.name "Update Bot"
git add data/
git commit -m "Update data files"
git push origin main
# push 後にローカルとリモートの先端を照合する
LOCAL_SHA=$(git rev-parse HEAD)
REMOTE_SHA=$(git ls-remote origin refs/heads/main | cut -f1)
if [ "$LOCAL_SHA" != "$REMOTE_SHA" ]; then
echo "ERROR: local=$LOCAL_SHA remote=$REMOTE_SHA 反映されていません" >&2
exit 1
fi
echo "OK: $LOCAL_SHA がリモートに反映されました"set -euo pipefail を冒頭に置けば、commit が失敗した時点でスクリプト全体が止まります。さらに SHA 照合があれば、想定外の経路で push が空振りしても必ず検出できます。
「コマンドが通ったか」ではなく「観測できる状態が変わったか」を見る。エージェントに作業を任せるうえで、この発想の転換がいちばん効きました。
対処2 — index.lock に阻まれる環境では REST API という選択肢
identity を設定しても、サンドボックス環境によっては .git/index.lock が残留し、git commit 自体が詰まることがあります。私の環境では並行して動く別プロセスとロックが衝突し、リトライしても解消しないケースがありました。
ロックファイルは「他に git プロセスが動いていないこと」を確認したうえで削除すれば復旧できます。
# 他に git プロセスがいないことを確認してから削除する
ps aux | grep -v grep | grep "git " || rm -f .git/index.lockただ、詰まるたびに手当てするのも不毛です。最終的に私は、この種のジョブを GitHub REST API で直接コミットを作る方式に切り替えました。ローカルの index を一切使わないため、identity 問題とロック問題の両方を構造的に回避できます。単一ファイルの更新なら次の流れで完結します。
#!/bin/bash
set -euo pipefail
OWNER="your-name"; REPO="your-repo"; BRANCH="main"
TOKEN="YOUR_GITHUB_TOKEN"
API="https://api.github.com/repos/$OWNER/$REPO"
AUTH="Authorization: Bearer $TOKEN"
# 1) 現在の先端コミットと、そのツリーを取得
HEAD_SHA=$(curl -s -H "$AUTH" "$API/git/ref/heads/$BRANCH" | jq -r '.object.sha')
TREE_SHA=$(curl -s -H "$AUTH" "$API/git/commits/$HEAD_SHA" | jq -r '.tree.sha')
# 2) 新しい内容で blob → tree → commit を作成
BLOB_SHA=$(jq -n --rawfile c data.json '{content:$c, encoding:"utf-8"}' \
| curl -s -X POST -H "$AUTH" -d @- "$API/git/blobs" | jq -r '.sha')
NEW_TREE=$(jq -n --arg base "$TREE_SHA" --arg b "$BLOB_SHA" \
'{base_tree:$base, tree:[{path:"data.json", mode:"100644", type:"blob", sha:$b}]}' \
| curl -s -X POST -H "$AUTH" -d @- "$API/git/trees" | jq -r '.sha')
NEW_COMMIT=$(jq -n --arg t "$NEW_TREE" --arg p "$HEAD_SHA" --arg m "Update data.json" \
'{message:$m, tree:$t, parents:[$p]}' \
| curl -s -X POST -H "$AUTH" -d @- "$API/git/commits" | jq -r '.sha')
# 3) ブランチの先端を新しいコミットへ進める
curl -s -X PATCH -H "$AUTH" -d "{\"sha\":\"$NEW_COMMIT\"}" \
"$API/git/refs/heads/$BRANCH" | jq -r '.object.sha'コミットの author はトークンの持ち主として記録されるため、user.name の設定は不要です。push に相当する処理は最後の refs 更新だけなので、失敗したときの原因の切り分けも簡単になりました。
予防策 — エージェントの「成功」を疑う仕組みを先に作る
同じ転び方をしないために、いまは次の3点を運用に組み込んでいます。
- クローン直後の identity 設定を手順書に固定で入れる。 エージェントへ渡す指示の最初に、毎回必ず置いています。
- 成功判定を SHA 照合に一本化する。 終了コードや「push しました」という自己申告は、判定材料に使いません。
set -euo pipefailを全スクリプトの1行目に置く。 静かに先へ進んでしまう失敗を、その場で止まる失敗に変えます。
エージェントの報告は、検証可能な事実と突き合わせてはじめて信頼できるものになります。自動化の便利さを保ったまま「疑う仕組み」を一枚足しておくことが、結局いちばんの近道でした。
挙動の裏付けを確認したいときは、git commit 公式ドキュメント、git push 公式ドキュメント、GitHub REST API — Git database が参考になります。
まずは手元の自動化スクリプトに、SHA 照合の数行を足すところから試してみてください。同じ症状に向き合っている方の手がかりになれば幸いです。