On this page
You ran an ordinary Git command and got a refusal with a file path
in it: Unable to create '.git/index.lock': File exists. Half the
advice online says delete the file and get on with your day. The
other half warns that deleting lock files is how repositories get
corrupted. The short answer: confirm no Git process is alive in that
repository, then delete the file. Your work is not inside it.
The rest of this post is why that answer holds and what the check protects you from, measured on Git 2.51 (Windows build, stock configuration, driven from Git Bash; platform spellings that differ are called out in place). The demos below are one continuous session; every command shows its complete output; only machine paths are shortened.
The lock is a draft of your index
A small repository, one change pending:
$ git status -sb
## main
M parser.js
$ git log --oneline
74f695c Add the parser
To look at the lock while it exists, hold Git open mid-commit. A
git commit -a keeps .git/index.lock for as long as its editor is
up, so a stand-in editor that just waits gives us all the time we
need. The & and echo $! are the one-terminal version of a
story you know: the editor window is open, and you are doing
something else in the foreground.
$ printf '%s\n' '#!/bin/sh' 'sleep 60' > slow-editor
$ chmod +x slow-editor
$ GIT_EDITOR=./slow-editor git commit -a &
$ echo $!
3232059
$ ls -l .git/index .git/index.lock
-rw-r--r-- 1 Evan 197121 137 Sep 14 01:14 .git/index
-rw-r--r-- 1 Evan 197121 137 Sep 14 01:14 .git/index.lock
Two files, side by side. .git/index is the staging area you know:
the table Git consults to answer git status and to build the next
commit. index.lock is the replacement table, being written in
full beside the original. When the operation succeeds, Git renames
the finished replacement over the original in one atomic step, and
the lock name disappears. Until then, the lock file’s existence is
the lock: creating it is how a writer claims the index, and a
second writer’s attempt to create it fails, which is the refusal
that brought you here.
That layout is the whole safety argument. The file under the lock
name is a half-built copy of a bookkeeping table. Your commits live
in the object store, your edits live in the working tree, and the
original index is still right there, untouched. Deleting a stale
index.lock discards a draft.
One scoping note, because it changes where you meet this error:
git commit -a holds the lock across the editor because it has to
refresh the index before it can commit it. A plain git commit of
work you already staged shows no lock at all while its editor is
open (measured separately, same stand-in editor). The long-lived
lock belongs to the operations that rewrite the index.
Endings Git can see, and endings it can’t
End it politely. In this one-terminal setup the signal lands on the waiting editor, and Git is still alive to see its editor die:
$ kill 3232059
Aborting commit due to empty commit message.
$ ls .git/index.lock
ls: cannot access '.git/index.lock': No such file or directory
$ git status --short
M parser.js
?? slow-editor
The editor died; Git woke to an empty message, aborted the commit, and removed its lock on the way out. A crashed editor is an ending Git attends — so is a finish, a failure, or a signal Git itself can catch (Ctrl-C included; its signal handlers run the same cleanup). Every ending Git sees, it sweeps up after.
Now the ending Git cannot attend. kill -9 is the reproducible
stand-in for the power cut, the out-of-memory killer, Task Manager’s
End task, the laptop that went to sleep with a hook half-run:
$ GIT_EDITOR=./slow-editor git commit -a &
$ echo $!
3232071
$ kill -9 3232071
$ ls -l .git/index.lock
-rw-r--r-- 1 Evan 197121 137 Sep 14 01:14 .git/index.lock
No process, but the claim file is still on disk. That is the entire phenomenon: a lock on disk means either a Git process is alive and using it right now, or one died without the chance to clean up. The safety question is telling those two apart, and nothing in the file itself will tell you — it’s the same 137 bytes either way.
What still works under a stale lock
The next write meets the claim file and refuses:
$ git add parser.js
fatal: Unable to create '…/notes/.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.
$ git status --short
M parser.js
?? slow-editor
$ git log --oneline
74f695c Add the parser
Read the message again, slowly: it is the correct procedure in two sentences. Make sure no process is running, and only then remove the file by hand. Everything below is the evidence for why those two steps, in that order, are right.
Notice also what still works. git status, git diff, git log
all ran fine against the held lock, and a fetch lands normally —
none of them need to replace the index. The refusals belong to the
commands that rewrite the staging area — add above, and commit
and git stash measured against the same held lock. A stale lock is
a roadblock on one street, not a frozen repository.
One check, then delete
The check is the first sentence of Git’s advice: is any Git
process alive here? From PowerShell or cmd the spelling is
tasklist /FI "IMAGENAME eq git.exe"; elsewhere it’s pgrep git;
the Git Bash session below counts the same thing with a pipeline:
$ tasklist | grep -c git.exe
0
$ rm .git/index.lock
$ git status --short
M parser.js
?? slow-editor
$ git fsck --no-progress
dangling tree 777441cdba5ae9f9e6c3b1af0ae7d7bfceef2f45
$ git commit -am "Handle empty input"
[main 757d2dd] Handle empty input
1 file changed, 1 insertion(+)
Zero Git processes on the whole machine, so the owner of that lock
is gone and the file is a leftover. A nonzero count is not the
opposite verdict, because the count is machine-wide while the
question is about one repository: another repo mid-operation, an
fsmonitor daemon (core.fsmonitor keeps one alive per watched
repository), or a prompt daemon that happens to match pgrep git
will all hold the number above zero on a perfectly quiet setup.
Treat a nonzero answer as a suspect list, and read each survivor’s
command line to see what it is: pgrep -a git on Linux,
pgrep -fl git on macOS, or in PowerShell the one-liner
gcim Win32_Process -Filter "Name='git.exe'" | fl ProcessId,CommandLine.
Daemons announce themselves there. The one thing a command line won’t
reliably name is the repository (a git launched from inside one
carries no path in its arguments), so close anything you can’t
account for before trusting the delete. With the owner provably
gone, delete it (del .git\index.lock from PowerShell or cmd) and
take inventory: the pending change is still pending, the history
is intact, and the commit that was interrupted two sections ago
never happened — Git doesn’t half-commit.
The one trace the crash left is that dangling tree: a scratch object the dying commit wrote, referenced by nothing, sitting in the same unreferenced limbo a dropped stash sits in until garbage collection sweeps it. That’s the complete damage report for a hard kill plus a hand-deleted lock: one orphan object and a finished cup of coffee.
When you run the check, remember the suspects that don’t look like
Git: the editor window from the error message’s own example, an IDE
whose Git integration is mid-refresh, a hook that is still running,
a desktop client doing background work. Desktop clients deserve one
extra suspicion: some embed Git as a library and hold the lock with
no git process for the task list to find, so close those outright
rather than counting them. The process you’re looking for was
usually started by something you think of as idle.
One path note: everything above says .git/index.lock because the
demo is a plain clone. In a linked worktree each checkout keeps its
own index, so its lock lives under the main repository’s .git
directory instead. There is nothing to memorize: the path in
Git’s error message is the real one, and the command
git rev-parse --git-path index.lock prints it wherever you stand.
Why the check comes first
Here is the same delete with the check skipped — the lock’s owner is alive. Nothing pushes back:
$ printf '// TODO: sort keys before writing\n' >> parser.js
$ printf '%s\n' '#!/bin/sh' 'echo "Sort keys before writing" > "$1"' 'sleep 30' > slow-editor2
$ chmod +x slow-editor2
$ GIT_EDITOR=./slow-editor2 git commit -a &
$ echo $!
3232101
$ rm .git/index.lock
$ printf 'todo\n' > notes.md
$ git add notes.md
$ git status --short
A notes.md
M parser.js
?? slow-editor
?? slow-editor2
The rm succeeded without a murmur, and the git add right after
it walked straight in and staged a file — with a commit still in
flight in the background. The mutual exclusion is gone; two writers
now hold two different ideas of what the index says. When the first
one finishes, the ideas collide:
$ wait
fatal: repository has been updated, but unable to write
new index file. Check that disk is not full and quota is
not exceeded, and then "git restore --staged :/" to recover.
$ git log --oneline
72fdf62 Sort keys before writing
757d2dd Handle empty input
74f695c Add the parser
$ git status --short
A notes.md
MM parser.js
?? slow-editor
?? slow-editor2
$ git restore --staged :/
$ git status --short
?? notes.md
?? slow-editor
?? slow-editor2
Take that apart. The commit landed (git log shows it), but the
committing process lost the race to write the index it believed
in, so it died pointing at a disk-full that never happened.
git status now reports MM on a file that was committed one
command ago: staged changes that differ from the commit and a
working tree that differs from the staging. No line of anyone’s
work was lost, and Git’s own printed recovery put the index back
in one command — though read it before you reuse it: the recovery
runs git restore --staged :/, which unstages everything in the
repository, the deliberate add included; that is notes.md
dropping back to untracked on the last line. What the blind
delete bought was a torn scoreboard, a misleading error, and a
repair step — on a toy repository with one human typing slowly.
The check costs one command; this is what it buys.
Even git status writes through that lock
The second writer above was me, typing. In your repository it is more often software, and it doesn’t have to be doing anything that looks like a write. Sweep the demo scaffolding out of the way and watch the index file itself:
$ rm notes.md slow-editor slow-editor2
$ stat -c '%y' .git/index
2026-09-14 01:15:18.521557500 -0400
$ touch parser.js
$ git status --short
$ stat -c '%y' .git/index
2026-09-14 01:15:18.653266700 -0400
A git status that printed nothing rewrote the index. (The
stat -c spelling is GNU; BSD and macOS spell it stat -f '%Sm'.)
The touched file made Git re-check its cached file metadata, and
it saved the refreshed cache the only way it saves anything: new
table under the lock name, atomic rename. Git treats that
particular lock as optional (a status that can’t get it just
skips the save) and gives tools a switch to opt out entirely:
$ touch parser.js
$ GIT_OPTIONAL_LOCKS=0 git status --short
$ stat -c '%y' .git/index
2026-09-14 01:15:18.653266700 -0400
Same silent status, index untouched. GIT_OPTIONAL_LOCKS=0 (or
git --no-optional-locks) exists for one audience: software that
runs Git in the background of your repository and has no business
competing with your hands for the index.
Or don’t do any of this
Look at the suspect list in Git’s error message one more time: “another git process seems to be running.” A desktop Git client is “another git process” for a living. It refreshes status every few seconds; it fetches while you read; it is the thing most likely to be holding, or meeting, that lock at the moment you get unlucky. The error message reads as an accusation of exactly my category of software, so I treat it as a conduct code for GitDesktop, in three clauses.
Every Git command the client itself runs goes through one spawn
path, and that path sets GIT_OPTIONAL_LOCKS=0. The background
status refresh you just watched rewrite an index cannot do that in
your repository from my client — reads stay reads.
When a single mutating command it runs on your behalf meets a lock someone else holds (your terminal, your editor, your hook), it waits 300ms and retries once before showing you Git’s message verbatim, because a lock that exists for the length of an editor’s save usually needs a beat of patience, not an error dialog.
And when it must kill a Git process of its own, say a network
operation past its deadline, on Windows it ties each spawned Git to
a job object, so the git shim on PATH and the real git.exe it
launches underneath die together instead of leaving an orphan
writer behind. A half-killed Git that keeps writing after the
client reported failure is the “crashed earlier” branch of that
error message, manufactured by tooling; the least a client
can do is make sure that when this post’s check says nothing
is running, nothing is.
None of that makes the stale lock impossible. Power still fails,
laptops still sleep at the wrong moment, and some other tool will
eventually die holding the claim. When it happens, you now know
exactly what is in that file: a draft of a table you can regenerate
with any git add, guarded by a name whose owner may be long gone.
The delete is safe the moment you can prove the last writer is gone; the file was always a draft wearing an important-sounding name.
Written while building GitDesktop.
A desktop Git client for GitHub, GitLab and Bitbucket — free and open source, with AI you control or hide entirely.