What this error means
Git refused to update a ref because its current value did not match the value Git expected when it started the update. This usually indicates:
- A concurrent Git operation updated the same ref.
- A stale lock file or packed-refs inconsistency.
- Case-insensitive filesystem ref collisions.
- Permissions or read-only files preventing ref updates.
You’ll typically see it during fetch/pull/push, e.g. updating refs/remotes/origin/<branch> or refs/heads/<branch>.
Quickstart (safe fixes)
Stop concurrent Git operations
- Close IDE auto-fetch, background watchers, or parallel CI steps.
Retry the operation
- Run:
git fetch --prune --tagsor rerun your original command.
- Run:
Remove stale lock files (only if no Git process is running)
- POSIX:
find .git -type f -name "*.lock" -print -delete - PowerShell:
Get-ChildItem -Recurse .git -Filter *.lock | Remove-Item -Force
- POSIX:
Normalize refs and clean up
git gc --prune=now --aggressive git pack-refs --all git fsck --full
Force-refresh remote-tracking refs (trusting the remote)
git fetch origin +refs/heads/*:refs/remotes/origin/* --prune --tags
If the error persists, follow the scenarios below.
Minimal working example (why the error happens)
This demonstrates two conflicting updates to the same ref.
# Start a scratch repo
rm -rf demo && mkdir demo && cd demo
git init
echo a > f && git add f && git commit -m "a"
old=$(git rev-parse HEAD)
# Create two different future commit IDs
git commit --allow-empty -m "b"; new1=$(git rev-parse HEAD)
git commit --allow-empty -m "c"; new2=$(git rev-parse HEAD)
# Create a ref at the old value
git update-ref refs/heads/tmp "$old"
# First update succeeds
git update-ref refs/heads/tmp "$new1" "$old"
# Second update expects the same old value, but the ref is now new1
# This reproduces: "cannot lock ref ... is at <new1> but expected <old>"
git update-ref refs/heads/tmp "$new2" "$old" || true
Root cause: Git performs an atomic compare-and-swap. If the ref changed since it was read, the update is aborted to prevent races.
Diagnose the failure
- Identify the failing ref from the message, e.g.
refs/remotes/origin/feature/x. - Check current value vs. expected (from the error):
git rev-parse refs/remotes/origin/feature/x 2>/dev/null || echo "ref missing" - Look for leftover locks:
find .git -type f -name "*.lock" - Detect case-colliding refs (on case-insensitive filesystems):
git for-each-ref --format='%(refname)' | awk '{print tolower($0)}' | sort | uniq -d - Verify repository health:
git fsck --full
Scenario-based fixes
- Concurrent fetch/pull/push
- Symptom: Occurs intermittently; rerun often succeeds.
- Fix:
- Ensure only one Git process runs per repo at a time.
- Retry:
git fetch --prune --tags. - In CI, serialize steps or use a clean clone per job.
- Stale lock files
- Symptom:
.lockfiles remain after a crash or kill. - Fix:
- Ensure no Git processes are running.
- Delete
*.lockunder.git/(see Quickstart step 3).
- Packed-refs vs. loose refs mismatch
- Symptom: Repeated errors on fetch; refs appear both in
.git/refs/*and.git/packed-refs. - Fix:
git pack-refs --all git gc --prune=now git fetch --prune --tags
- Case-colliding refs (Windows/macOS default FS)
- Symptom: Branches or tags differ only by case (e.g.,
Featurevsfeature). - Detect and resolve:
git for-each-ref --format='%(refname)' | sort -f | uniq -d # Rename on the remote and locally to a unique, canonical name git branch -m Feature feature git push origin :Feature feature git fetch --prune - Consider enforcing naming rules in reviews/CI.
- Permissions/read-only files
- Symptom: Error persists;
.git/is not writable by your user. - Fix:
- POSIX:
chown -R "$USER" .git && chmod -R u+rwX .git - Windows: Unset read-only attributes on
.gitand its children.
- POSIX:
- Force-refresh remote-tracking refs (trust remote state)
- Symptom: Only remote-tracking refs fail (e.g.,
refs/remotes/origin/*). - Fix:
The leadinggit fetch origin +refs/heads/*:refs/remotes/origin/* --prune --tags+forces updates even if they are non-fast-forward.
- Broken or corrupt refs
- Symptom:
git fsckreports issues; ref points to missing object. - Fix:
- If safe to delete a remote-tracking ref:
git update-ref -d refs/remotes/origin/feature/x git fetch --prune - If a local branch is corrupt, create a new branch from a known good commit and delete the bad one.
- If safe to delete a remote-tracking ref:
Preventing recurrence
- Enable pruning and avoid stale remote refs:
git config fetch.prune true git config fetch.pruneTags true - Avoid parallel Git operations on the same working directory.
- Keep the repo healthy periodically:
git maintenance run --task=gc - Enforce branch naming to avoid case collisions; consider
receive.denyDeleteCurrentandreceive.denyNonFastForwardson servers.
Performance notes
git gc --aggressiveis CPU/IO intensive; use sparingly on large repos or run during off-peak times.git fsck --fullcan be slow on big histories; run for diagnostics, not on every CI run.- For CI, prefer clean clones over heavy maintenance; they are often faster and more reliable.
Pitfalls
- Deleting
.lockfiles while a Git command is running can corrupt refs. Always stop all Git processes first. - Forcing ref updates (
+in fetch) can move refs backward. Only do this when you trust the remote. - Case-colliding refs might reappear if server and clients use different filesystems. Standardize branch naming.
Tiny FAQ
Why does this happen?
- A ref changed between read and write, or Git could not safely create/update the ref due to locks, collisions, or permissions.
Is deleting
.lockfiles safe?- Yes, if you are certain no Git process is running. Otherwise, wait or kill the process first.
I only see this on Windows/macOS. Why?
- Case-insensitive filesystems can introduce ref name collisions. Also, antivirus or file indexers can transiently lock files.
How do I fix it on a server-side bare repo?
- Stop all hooks/operations using that repo, remove stale locks, run
git gc --prune=now, verify withgit fsck, then retry the push.
- Stop all hooks/operations using that repo, remove stale locks, run