Recover Staged Changes Lost by git checkout HEAD Using git fsck¶
Audience: Anyone who just lost git add-ed changes and is panicking
Key Points¶
- Why changes disappear
git checkout HEAD -- <path>resets both the index and working tree to HEAD - Why recovery is possible
git addsaves file contents as blob objects that persist even after losing references - Recovery steps
git fsck --lost-found→ identify blobs → restore files
The Typical Accident¶
You've been refactoring 15 files, all carefully staged with git add. Then you run this to undo a separate bulk replacement:
git checkout HEAD -- docs/
Every staged change is gone. git status shows clean. Nothing in git log.
If you ran git add, your data can be recovered. As long as git gc hasn't run, the data is still there. If you're in a hurry, jump to Recovery Steps (Step 1).
Recovery prerequisite
git gc must not have run since the accident. git gc deletes unreferenced objects. If you notice the problem, never run git gc.
Why Changes Disappear, and Why Data Remains¶
git checkout HEAD -- <path> does two things simultaneously:
- Resets the index to HEAD — your
git add-ed content is unstaged - Resets the working tree to HEAD — file contents revert to the last commit
However, Git saves file contents as blob objects in .git/objects/ at the moment you run git add. When the index stops referencing these blobs, they become dangling blobs — unreferenced objects that Git hasn't deleted yet.
Your data is still inside .git/objects/. The challenge is identifying which blob belongs to which file.
Recovery Steps¶
Step 1: Extract dangling blobs¶
git fsck --lost-found
Dangling blobs and dangling trees are written as files to .git/lost-found/other/ (dangling commits go to .git/lost-found/commit/ separately).
$ ls .git/lost-found/other/ | wc -l
314
The count depends on repository history. Tens to hundreds of files is typical. Now you need to find yours among them.
Check the count first
Use git fsck --dangling to see counts and types without writing files:
git fsck --dangling 2>&1 | head -10
Step 2: Identify your blobs¶
Two approaches work well for finding specific files among hundreds of candidates:
- Approach A (keyword search) — best when you remember any string from the file contents
- Approach B (size comparison) — best for files with no distinctive content or near-binary files
Combining both approaches improves accuracy.
Approach A: Search by file content keywords¶
Grep for any string you remember from the file — function names, class names, variable names, comments, or headings all work.
# Search for a remembered string
for blob in $(ls .git/lost-found/other/); do
if grep -ql 'handleSubmit' \
".git/lost-found/other/$blob" 2>/dev/null; then
echo "FOUND: $blob"
fi
done
If multiple candidates match, inspect the first few lines and size to narrow down:
# Show first 5 lines and size for each candidate
for blob in $(ls .git/lost-found/other/); do
if grep -ql 'handleSubmit' \
".git/lost-found/other/$blob" 2>/dev/null; then
size=$(wc -c < ".git/lost-found/other/$blob")
echo "=== $blob ($size bytes) ==="
head -5 ".git/lost-found/other/$blob"
echo
fi
done
Approach B: Narrow candidates by HEAD blob size¶
Use the HEAD version's file size as a baseline to narrow candidates:
# Get HEAD blob size
head_size=$(git cat-file -s \
$(git rev-parse HEAD:"path/to/file.md"))
echo "HEAD: $head_size bytes"
# Find blobs with similar size
for blob in $(ls .git/lost-found/other/); do
size=$(wc -c < ".git/lost-found/other/$blob")
if [ "$size" -gt $((head_size - 2000)) ] \
&& [ "$size" -lt $((head_size + 5000)) ]; then
echo "$blob ($size bytes)"
fi
done
When multiple candidates share the same title, the largest one is most likely the latest version. Avoid any version containing merge conflict markers (<<<<<<<).
Step 3: Restore¶
Once identified, a simple copy restores the file:
cp .git/lost-found/other/<blob_hash> path/to/file.md
Verify the diff matches your original changes:
git diff path/to/file.md
When Recovery Is Not Possible¶
| Situation | Recoverable? |
|---|---|
Immediately after git checkout HEAD | Yes |
After git gc | No |
After git gc --prune=now | No |
| Weeks later (auto-gc may have run) | Check first |
Changes never staged with git add | No |
The last row matters most. Unstaged working tree changes are never saved to Git's object store, so no recovery mechanism exists for them.
Prevention: Use git stash Before Destructive Operations¶
The simplest prevention is one extra command before any bulk file operation:
# 1. Save staged changes
git stash --include-untracked -m "before bulk operation"
# 2. Safely reset files
git checkout HEAD -- docs/
# 3. Restore saved changes
git stash pop
git stash preserves both the index and working tree, making git stash pop a complete restore.
Understanding Git's Object Model Is Your Safety Net¶
git add → blob saved → index references blob. Knowing this flow leads to the insight that "blobs persist after losing index references."
Understanding Git internals isn't about faster daily operations. It's about staying calm when things go wrong.
Related: When you want blobs to disappear
Blob persistence enables recovery, but it also means data you want deleted lingers. If you accidentally committed API keys, see Secrets Exposure Recovery Guide for the removal procedure.
FAQ¶
Q: Can I recover changes that were git add-ed?
Yes, as long as git gc hasn't run. When you git add a file, Git saves its contents as a blob object. Even after the index stops referencing the blob, the blob itself remains in .git/objects/ and can be extracted with git fsck --lost-found.
Q: Can I recover after git gc?
No. git gc deletes unreferenced objects (dangling blobs). By default it targets objects older than 2 weeks, but git gc --prune=now deletes everything immediately.
Q: What about changes that were never staged with git add?
Not recoverable. Unstaged working tree changes are never written to Git's object store, so no tool can recover them. Frequent git add is your best insurance.