So there I was, trying to do a simple git fetch before a standup, and my terminal just barfs this at me:
error: cannot lock ref 'refs/remotes/origin/main': is at abc1234 but expected def5678
From https://github.com/myorg/myrepo
! [new branch] feature/login -> origin/feature/login (unable to update local ref)
The git error: cannot lock ref is one of those things that sounds catastrophic but usually isn't. Usually. I've seen this pop up on dev machines that haven't been synced in a while, on CI pipelines that get interrupted mid-run, and on repos that have been passed around between team members like a hot potato. Once you understand what's actually happening, fixing it is pretty straightforward.
Why This Happens in the First Place
Git stores your remote-tracking branches as files inside .git/refs/remotes/. When it tries to update those refs during a fetch or pull, it creates a temporary lock file — think of it like Git saying "hold on, I'm writing to this file, don't touch it." Normally that lock file disappears once the write is done.
But when a process gets killed mid-operation — a failed CI job, a forced terminal close, a laptop that decided it was nap time — that lock file sticks around. Next time you try to fetch, Git sees the lock file and refuses to proceed because it thinks another operation is already in progress. It's not. It's just a ghost.
There's also a second scenario that trips people up: a branch name conflict. If someone on your team created a branch called feature/login and at some point there was also a file or directory at that ref path, Git gets confused because the filesystem can't have a file and a directory with the same name at the same path. Happens more than you'd think when branches get renamed or deleted remotely but your local refs are stale.
Fix #1 — Delete the Stale Lock Files
This is the fix for 80% of cases. Go find the lock files and nuke them.
find .git/refs -name "*.lock" -delete
Or if you're on Windows in Git Bash or PowerShell, you can do it manually — navigate to .git/refs/remotes/origin/ and look for any file ending in .lock. Delete those files, then retry your fetch.
git fetch --all
Honestly, nine times out of ten this is all you need. If you're seeing this in a CI/CD pipeline, it's worth adding a cleanup step before your fetch command to prevent it from recurring.
Fix #2 — Prune and Force-Fetch Stale Refs
If deleting the lock files doesn't fully solve it — maybe you're still getting the error on specific branches — the problem is probably stale remote-tracking refs. Branches that got deleted on the remote but are still hanging around locally as ghost references.
Run this:
git fetch --prune
The --prune flag tells Git to remove any remote-tracking references that no longer exist on the remote. This is something I honestly think should just be the default behavior, but here we are.
If a specific branch ref is the problem (say refs/remotes/origin/feature/login), you can also manually delete just that one:
git update-ref -d refs/remotes/origin/feature/login
Then try your fetch again. This is especially useful when you know exactly which branch is causing the conflict — maybe someone renamed a branch on GitHub and now your local copy is fighting with the new name.
Fix #3 — Run git fsck and Clean Up the Ref Store
Now here's where it gets tricky. Sometimes the issue is a bit deeper — corrupted or inconsistent ref files, not just stale ones. I've seen this after bad merges or after someone did something creative with git filter-branch (please don't use that tool, use git filter-repo instead, but that's a whole other post).
Run a quick sanity check first:
git fsck --full
This scans your object database for issues. If you see errors about missing objects or broken links, you've got a deeper problem. But if it comes back mostly clean with just some dangling commits (that's normal, don't panic), then try packing your refs:
git pack-refs --all --prune
What this does is consolidate all your loose ref files into a single packed-refs file. It can resolve conflicts caused by filesystem weirdness, especially on case-insensitive filesystems like macOS and Windows — another fun edge case where feature/Login and feature/login look the same to the OS but different to Git.
The Fallback: When All Else Fails, Re-Clone
I know, I know. Nobody wants to hear "just re-clone." But if you're on a CI server or working in a Docker container and you can't get the repo into a clean state, it's genuinely faster to just wipe the workspace and start fresh than to keep debugging ref corruption.
rm -rf .git
git init
git remote add origin https://github.com/yourorg/yourrepo.git
git fetch --all
git checkout main
Or more brutally, just delete the whole directory and clone again. On ephemeral build environments this should actually be your first move, not your last resort. Local disk space on a CI runner is cheap; your time is not.
One thing worth doing if you're on a shared dev machine — set this in your global git config so pruning happens automatically every time you fetch:
git config --global fetch.prune true
Small change, prevents a whole category of stale-ref headaches before they start. Hope this saves you from losing half a morning to what is, at the end of the day, just some leftover lock files being dramatic.
๋๊ธ
๋๊ธ ์ฐ๊ธฐ