「Claude Code に git push までお願いしたら、fatal: Unable to create '.git/index.lock': File exists. で止まったまま動かない」——この症状は私も自動化スクリプトを書き始めて最初に踏んだ落とし穴でした。ローカル環境ではまず再現しないのに、VM や Docker、CI のサンドボックスに入れた途端に発生します。
このエラーの厄介なところは、画面上は「ロックファイルを消せばいい」と書かれているのに、消しても次のコマンドでまた同じ場所で止まることです。原因は単一ではなく、所有権・前回プロセスの残骸・ファイルシステムの遅延 という3つの層が重なって発生しているケースがほとんどです。
私は同じ症状を Cowork mode のスケジュールタスクで何度も踏み、最終的に「ロックファイルと戦わない」設計に切り替えて解決しました。ここではその経験を踏まえて、VM・CI 環境で再現性の高い3つの回避策を順番に紹介します。
なぜ Claude Code の git 操作だけが詰まるのか
ローカルの開発機で同じコマンドを叩いても発生しないため、つい「Claude Code のバグ」と疑いたくなります。しかし実際には Claude Code は内部で git コマンドをそのまま呼んでいるだけで、ロック競合の原因はほぼ環境側にあります。
代表的な原因は次の3つです。
- 所有権のズレ: VM やコンテナの
/tmpや作業ディレクトリがnobody:nogroupのような特殊な所有者で作成されており、Claude Code が動かしているユーザー(多くは root か通常ユーザー)から書き込めない - 前回 bash セッションのゾンビプロセス: bash ツールの呼び出しは独立したセッションで動くため、前回の
git commitが完了せずに終了した場合、ロックファイルだけが残る - ファイルシステムの flush 遅延: シャドウマウントやネットワークボリューム上で
rmを実行しても、次のgitコマンドからはまだファイルが見える瞬間がある
特に Cowork mode のように毎回新しい bash セッションが立ち上がる環境では、2 つ目の「ゾンビプロセスの残骸」が一番頻繁です。サンドボックスの制約上、ps でプロセスを掃除する権限がないため、対症療法的に rm で消す形になります。
回避策1: ロックファイルを安全に消してから再実行する
最初に試すべきは、.git/index.lock と .git/HEAD.lock をまとめて削除してから git の状態をリセットする方法です。Claude Code に頼むなら、コミットの前段に必ず以下を挟むようにしてください。
# 1. ロックファイルが存在しても無視して削除
find .git -maxdepth 2 -name "*.lock" -type f -delete 2>/dev/null
# 2. 不整合な index を強制リセット
git reset --mixed HEAD 2>/dev/null
# 3. ファイルシステムの flush を待つ(NFS / シャドウマウント対策)
sync
sleep 1
# 4. 通常通りコミット
git add content/
git commit -m "Add: 新着記事"find で *.lock を一括削除しているのは、index.lock だけでなく HEAD.lock や refs/heads/main.lock も同時に残っていることがあるためです。-maxdepth 2 で .git/objects/ 配下の正常なロックを巻き込まないようにしている点もポイントです。
このパターンは、ローカル開発と同じスクリプトを Claude Code に渡したい場合に最小の変更で済みます。週に数回起きる程度の散発的な症状ならこれで十分です。
回避策2: 作業ディレクトリを書き込み可能なホームに移す
「ロック削除を挟んでも翌日また同じところで止まる」場合、/tmp/repos の所有権そのものが書き込み不可になっている可能性が高いです。VM が再起動するたびに /tmp がクリアされ、別のプロセスが先にディレクトリを作成してしまうことがあるためです。
私は最終的に、/tmp 配下が使えない時のフォールバック先として $HOME/repos を用意しました。クローンの直前に書き込みテストを入れると、原因不明の停止がほぼゼロになります。
WORK="/tmp/repos/myproject"
# 書き込みテスト: 失敗したら $HOME/repos にフォールバック
mkdir -p "$(dirname "$WORK")" 2>/dev/null
if ! touch "$(dirname "$WORK")/.write_test" 2>/dev/null; then
echo "⚠️ /tmp/repos が書き込み不可 — $HOME/repos にフォールバック"
WORK="$HOME/repos/myproject"
mkdir -p "$HOME/repos"
fi
rm -f "$(dirname "$WORK")/.write_test" 2>/dev/null
# 以降、$WORK を作業ディレクトリとして使う
git clone --depth 1 "$REPO_URL" "$WORK"
cd "$WORK"この設計の効きどころは、VM が再起動して /tmp/repos が nobody 所有で作成されてしまった日だけ自動でホームに切り替わることです。私は Claude Code の Bash ツール実行エラーの修正方法 と組み合わせる形で運用していて、サンドボックスの状態に依存しない自動化が安定して動くようになりました。
回避策3: そもそもローカル git を経由せず GitHub REST API で push する
ロック削除でもダメ、フォールバックでもダメな環境(極端にディスクが逼迫している、git バイナリが古い、など)では、私はローカル git 自体を諦めて GitHub REST API で直接 push する方式に切り替えました。コードは少し長くなりますが、.git/index.lock の存在しない世界に行けるので、ロック競合が原理的に起こりません。
# 環境変数: GITHUB_TOKEN, OWNER, REPO, BRANCH=main
TOKEN="$GITHUB_TOKEN"
OWNER="masakihirokawa"
REPO="myproject"
BRANCH="main"
API="https://api.github.com/repos/${OWNER}/${REPO}"
# 1. ファイル内容を base64 で blob として登録
CONTENT=$(base64 -w0 < content/articles/ja/sample.mdx)
BLOB_SHA=$(curl -sS -X POST -H "Authorization: token $TOKEN" \
-d "{\"content\":\"${CONTENT}\",\"encoding\":\"base64\"}" \
"${API}/git/blobs" | jq -r .sha)
# 2. 現在のブランチの tree SHA を取得
PARENT_SHA=$(curl -sS -H "Authorization: token $TOKEN" \
"${API}/git/refs/heads/${BRANCH}" | jq -r .object.sha)
BASE_TREE=$(curl -sS -H "Authorization: token $TOKEN" \
"${API}/git/commits/${PARENT_SHA}" | jq -r .tree.sha)
# 3. 新しい tree を作成
TREE_SHA=$(curl -sS -X POST -H "Authorization: token $TOKEN" \
-d "{\"base_tree\":\"${BASE_TREE}\",\"tree\":[{\"path\":\"content/articles/ja/sample.mdx\",\"mode\":\"100644\",\"type\":\"blob\",\"sha\":\"${BLOB_SHA}\"}]}" \
"${API}/git/trees" | jq -r .sha)
# 4. commit を作って ref を進める
COMMIT_SHA=$(curl -sS -X POST -H "Authorization: token $TOKEN" \
-d "{\"message\":\"Add sample\",\"tree\":\"${TREE_SHA}\",\"parents\":[\"${PARENT_SHA}\"]}" \
"${API}/git/commits" | jq -r .sha)
curl -sS -X PATCH -H "Authorization: token $TOKEN" \
-d "{\"sha\":\"${COMMIT_SHA}\"}" \
"${API}/git/refs/heads/${BRANCH}"この4ステップは GitHub の Git Data API の典型的な使い方で、Blobs → Trees → Commits → Refs の順に組み立てる形になります。1ファイルだけなら 5 秒もかからず完了し、.git ディレクトリも不要です。
私は失敗が許されない自動化(毎日決まった時刻に走るスケジュールタスクなど)でこの方式を採用しています。普段の開発まで API push にする必要はありませんが、「自動化が落ちると翌朝困る」種類のジョブには非常に向いています。さらに踏み込んで設計したい方には Claude Code の git push 失敗を体系的に直す と Claude Code 自動化を本番で安定させる長時間タスク設計 を読み合わせるとイメージが掴みやすいです。
どの順番で試すかの判断基準
3つの回避策はそれぞれ手間と汎用性のトレードオフが違います。私は次の順番で導入することをおすすめしています。
- 週1回程度の頻度で起きる散発的な症状: 回避策1(ロック削除+reset)だけで十分。ローカル開発との差分が一番小さい
- VM 再起動後に高確率で起きる: 回避策2(書き込み可能なディレクトリへフォールバック)を追加。所有権問題は単発の rm では解消しないため
- 絶対に止めたくない自動化ジョブ: 回避策3(GitHub REST API)に切り替える。ローカル git を経由しないため、ロック競合・所有権・ファイルシステム遅延のすべてを回避できる
この見極めができないまま全部を一度に入れてしまうと、コードの可読性だけが落ちて運用コストが増えてしまいます。「今困っている症状はどのレイヤーから来ているか」を一つずつ切り分けるのが結果的に近道です。
Claude Code に渡す前提でワークフローを整理しておくと、ロック競合のような「環境依存のトラブル」も体系的に潰しやすくなります。
まずは今動いている自動化スクリプトの git add の前に、回避策1の4行を挟むところから始めてみてください。これだけで翌週からの停止頻度が体感できるくらい変わります。