Claude Codeでindex.lockが残るときの対処 — Windowsの手順
Claude Codeのgit実行後に.git/index.lockが残りcommitが失敗するとき、Windowsでプロセスを確かめて安全に消す手順と、再発を減らす設定をまとめます。
Claude Codeにgit commitやgit resetを頼んだら、Unable to create '.git/index.lock': File exists.で止まった。そんなときは、まずgitのプロセスが本当に動いていないことを確かめ、動いていなければロックファイルを1つだけ消してコミットをやり直します。
index.lockは、gitがインデックス(ステージングの状態)を書き換えるあいだ作る排他ロックのファイルです。正常なら操作の終わりに消えます。残ったままだと、次のgit操作は「別のgitが動いている」とみなして失敗します。
この記事はWindowsで困っている人向けに、確認と削除の手順、残る理由として報告されている見立て、再発を減らす設定を順に扱います。元になっているのはanthropics/claude-codeのissue #28546です。
症状とエラーメッセージ
issueの報告者が貼っているエラーは次の形です。パスは環境ごとに変わります。
Unable to create 'C:/DATA/git/project/.git/index.lock': File exists.
Another git process seems to be running in this repository, e.g.
an editor opened by 'git commit'. Please make sure all processes
are terminated then try again. If it still fails, a git process
may have crashed in this repository earlier:
remove the file manually to continue.Claude CodeのログではError: Exit code 128の後ろにfatal: Unable to create ... File exists.が続く形で現れます。同じ文面が2回出る報告もあります。
issueの再現手順は3段です。
- WindowsでClaude Codeを開き、Bashツールにgitコマンド(
git status、git add、git commitなど)を実行させる - 直後にRiderやVS Code、別のターミナルへ切り替えてgitを実行する
index.lockのエラーが出る
報告者によると、同じgitコマンドを手動で打ったときは起きず、Claude CodeのBashツールが実行したときだけ起きたといいます。
1. 本当に残ったロックかを確かめる
エラーが出た時点で、別のgitが正当にロックを持っている場合があります。エディタのGit拡張が裏でstatusを走らせていることもあります。ロックを消す前に、動いているgitを探します。
PowerShellなら、次の2行で足ります。
Get-Process git -ErrorAction SilentlyContinue
Test-Path .git\index.lock1行目が何も返さず、2行目がTrueなら、ロックだけが残っています。1行目にプロセスが出るなら、そのgitが終わるまで待ちます。終わらないときは、そのプロセスがどのツールのものかを確かめてから止めます。
Git Bashを使っているなら、psで見る手もあります。
ps aux | grep -i "[g]it"
ls -l .git/index.lockWindowsではファイルを開いているあいだは削除できません。削除に失敗する場合は、どこかのプロセスがまだ握っています。無理に消さず、エディタやGit GUIを閉じてからやり直します。
2. 安全に削除してコミットをやり直す
確認が済んだら、ロックを消します。消すのはindex.lockの1ファイルだけです。.gitディレクトリごとの削除や、他のファイルの削除は不要です。
stale lockを消すまでの流れ
- 1
gitのプロセスを確かめる
Get-Process gitで何も出ないことを確かめます。エディタのGit機能も一度静かにさせます。 - 2
ロックの場所を確かめる
通常のリポジトリは
.git\index.lockです。git worktreeでは.git\worktrees\<名前>\index.lockになります。 - 3
1ファイルだけ削除する
PowerShellで
Remove-Itemを使います。 - 4
gitで状態を見てから再実行する
git statusが通るのを見て、止まっていた操作をやり直します。
Remove-Item .git\index.lock
git statusworktreeを使っている場合は、場所の取り違えが落とし穴になります。issueでは、worktree配下のロックは.git/worktrees/<name>/index.lockで、.git/index.lockだけを見るクリーンアップ処理では見つからないと指摘されています。git rev-parse --git-dirを実行すると、いま居るworktreeの.gitの実体が分かります。
git rev-parse --git-dir消しても直後にまた現れるとき
issueには、消した直後に同じロックが再び現れたという報告があります。rmとgit reset --hardを同じ&&のチェーンで実行すると、その間にロックが作り直され、同じエラーで失敗したそうです。削除とgit操作を別々のBashツール呼び出しに分けたときだけ成功したとあります。
もう1件、Windowsの報告者は、bashのrmが一時的なロックファイルの削除に失敗しやすかったため、PowerShellで削除してから再実行するリトライ用のラッパーを作っています。Git Bashのrmが通らないときは、PowerShellのRemove-Itemに切り替えると試しやすい手段です。
なぜ残るのか — issueで出ている見立て
原因は、issueが開いている時点でも確定していません。ここでは報告された事実と、報告者の見立てを分けて並べます。
| 観点 | issueにある内容 |
|---|---|
| 起きる環境 | issueにある内容Windows(Git Bash)の報告から始まり、WSL2、macOS、Ubuntuの報告も続いている |
| 起点の見立て | issueにある内容Windowsはファイルハンドルの解放が遅く、子プロセス終了後もロックが残るのではないか(報告者の推測) |
| 定期的な生成 | issueにある内容PowerShellのFileSystemWatcherで監視すると、ロックが約10秒間隔で現れ、1回あたり約6ミリ秒で消えた(Windows 10、Claude Desktop v1.1617.0) |
| 対象 | issueにある内容監視した範囲では、アクティブなセッションのworktreeだけで発生した |
| 先行issue | issueにある内容#11005(Linux/macOS)はNOT_PLANNEDで閉じられている |
この報告者は、内部のポーリングがgit status --porcelainを使っているなら--no-optional-locksにするのが妥当な修正だと書いています。どの処理がロックを作っているかは、issue上で断定されていません。
見立てが2つに分かれている点が実務では重要です。
- 本当に消えない「stale lock」: 手動で消せば直る。消した後は再発するまで困らない
- 一瞬だけ現れる「競合」: タイミングが悪いとエラーになる。消すものが無いので、待ってリトライすれば通る
エラーが出た直後にロックを見に行くと、競合の場合は既に消えています。Test-PathがFalseなら、ロックを消す必要はなく、同じコマンドをもう一度流すだけで済みます。
再発を減らす運用
issueのコメントに出ている手を、効き方と注意点つきで並べます。
自作のstatusline・補助スクリプトにgitのオプションを付ける
あるコメント投稿者は、自分のstatusline用スクリプトがgit status --porcelainを呼び、それがコマンドと競合していたと報告しています。--no-optional-locksを付けると解消したそうです。
# 前
dirty=$(git -C "$cwd" status --porcelain 2>/dev/null)
# 後
dirty=$(git -C "$cwd" --no-optional-locks status --porcelain 2>/dev/null)gitのリファレンスでは、--no-optional-locksは「ロックを必要とする任意の操作をしない」オプションで、環境変数GIT_OPTIONAL_LOCKSを0にするのと同じと書かれています。たとえばgit statusがインデックスを更新し直す動作を抑えます。バックグラウンドで動き、他の操作とロックを奪い合いたくないプロセス向けのものです。
呼び出しが多いなら、環境変数のほうが手間が少なく済みます。issueでも、5か所のgit呼び出しを書き換えずにexport GIT_OPTIONAL_LOCKS=0で済ませた例が出ています。Claude Codeの設定では、settings.jsonのenvキーにその変数を置くと、セッションとそこから起動するサブプロセスに渡せます。
{
"env": {
"GIT_OPTIONAL_LOCKS": "0"
}
}envはsettings.jsonのどのスコープにも書けます。この変数を入れると、Claude CodeのBashツールが実行するgitにも効きます。git statusのあとにインデックスが更新されなくなるため、git statusを何度も叩く人は意識しておくと良い点です。コミット自体はロックが必要なので、この設定でgit commitが失敗しなくなるわけではありません。
statusline自体の組み方は、Claude Codeのstatuslineで使用量を出す設定に例があります。statuslineでgitを呼ぶ処理を足すときは、読み取りだけのgitに--no-optional-locksを付けておくと、コミットと競合しにくくなります。
PostToolUse hookで残骸を自動で消す
issueの別のコメントは、Bashのgitコマンドの後でロックを片付けるPostToolUse hookを紹介しています。ツール実行の成功後に走るhookで、matcherにBashを指定します。
{
"hooks": {
"PostToolUse": [
{
"matcher": "Bash",
"hooks": [
{
"type": "command",
"command": "bash ~/.claude/hooks/git-index-lock-cleanup.sh"
}
]
}
]
}
}スクリプトの骨子は、標準入力のJSONからtool_input.commandを取り出してgitコマンドかを判定し、git rev-parse --git-dirでロックの場所を求め、gitのプロセスが無ければ削除する、という流れです。元のコメントはpgrep -x gitでプロセスの有無を見ています。Git Bashにはpgrepが入っていない環境が多いので、Windowsでは次のようにtasklistを使う形に置き換えるのが現実的です。
#!/usr/bin/env bash
# PostToolUse(Bash)用の例。入力JSONにgitコマンドが含まれるときだけ動く
INPUT=$(cat)
COMMAND=$(echo "$INPUT" | jq -r '.tool_input.command // empty' 2>/dev/null)
[ -z "$COMMAND" ] && exit 0
echo "$COMMAND" | grep -qE '\bgit\b' || exit 0
GIT_DIR=$(git rev-parse --git-dir 2>/dev/null) || exit 0
LOCK="$GIT_DIR/index.lock"
[ -f "$LOCK" ] || exit 0
# 動いているgit.exeが無いときだけ消す
if ! tasklist //FI "IMAGENAME eq git.exe" 2>/dev/null | grep -qi "git.exe"; then
rm -f "$LOCK" 2>/dev/null
echo "INFO: removed stale index.lock" >&2
fi
exit 0jqが無い環境では先に導入が要ります。tasklist //FIの書き方はGit Bashでスラッシュがパス変換されないよう二重にしたものですが、環境ごとに挙動が違うため、手元で動作を見てから使ってください。
効果の限界も、issueに出ています。別のコメント投稿者は、このhookを入れても問題が残ったと書いています。hookはコマンドの後で走るため、コマンドの最中に起きた競合には間に合いません。なお、この例はexit 0で終えるので、削除に失敗してもClaude Codeの処理は止まりません。
hookのmatcherとif条件、入力JSONの形は、PostToolUseとStopでテストを自動実行する設計にも出てくる構造と同じです。
CLAUDE.mdで書き込み系gitの前に待ちを入れる
issueの別の投稿者は、~/.claude/CLAUDE.mdにgitの扱いを書いています。書き込み系のコマンドの前にsleep 0.1 && を付け、失敗したら0.5秒待って1回だけリトライする、という指示です。対象はgit commit、git add、git push、git checkout、git fetch、git rebase、git resetなどで、git statusやgit logのような読み取りには付けません。
同じ投稿者が、副作用も書いています。Claudeが関係の無いコマンドにもsleep 0.1 && を付けるようになり、その防止策がまた必要になったそうです。指示を足すほど副作用の管理が増えるため、先にGIT_OPTIONAL_LOCKSとstatuslineの見直しを試し、それでも残るときの補助にするのが無難です。
同時に触らない
issueの再現手順そのものが、Claude Codeの直後に別のツールからgitを呼ぶ形です。RiderやVS Codeの自動fetchや自動statusをいったん止める、Claudeにコミットを任せている間は別ツールでgitを操作しない、といった運用でも頻度は下がります。Gitフックが失敗を返す設計との組み合わせは、huskyとpre-commitでコミットの失敗をClaudeに直させる方法が参考になります。
症状から手を選ぶ
| 症状 | まず試すこと |
|---|---|
エラー直後でもTest-PathがFalse | まず試すこと何もせず、同じコマンドをもう一度流す |
Test-PathがTrue、gitプロセスなし | まず試すことRemove-Itemで1ファイルだけ消して再実行 |
| gitプロセスがいる | まず試すこと終わるまで待つ。エディタのGit機能を止める |
| 消してもすぐ現れる | まず試すこと削除とgit操作を別々の呼び出しに分ける |
| worktreeで見つからない | まず試すことgit rev-parse --git-dirでロックの場所を調べる |
| 繰り返し起きる | まず試すことstatusline・補助スクリプトに--no-optional-locksかGIT_OPTIONAL_LOCKS=0 |
Windows固有の補足
Git Bashを経由している場合の注意は、Windowsでコマンドが途中で切れる問題でも出てきます。Windows Git Bashでコマンドが約8KBで切れる原因と回避策と合わせて読むと、WindowsでのBashツールの癖がつかみやすくなります。
statuslineのドキュメントでは、Windowsのstatuslineコマンドは、Git Bashが入っていればGit Bash経由、無ければPowerShell経由で実行されます。スクリプトで使うパスはスラッシュで書く必要があります。バックスラッシュのままだと、区切りが失われてコマンドが失敗します。
まとめ
index.lockが残ったときの基本は、gitプロセスの有無を見る、ロックが存在するか見る、1ファイルだけ消す、の3点です。エラー直後にロックが無ければ、待って同じコマンドを流すだけで足ります。
再発を減らす手は、GIT_OPTIONAL_LOCKS=0か--no-optional-locksの導入が最初の一手です。hookやCLAUDE.mdのリトライは補助にとどまるという報告もあるため、試すなら手元で挙動を確かめてから常用に移すのが安全です。issueは開いたままなので、公式側の修正が入るまでは、この運用で付き合うことになります。