Skip to content

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 disappeargit checkout HEAD -- <path> resets both the index and working tree to HEAD
  • Why recovery is possiblegit add saves file contents as blob objects that persist even after losing references
  • Recovery stepsgit 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:

  1. Resets the index to HEAD — your git add-ed content is unstaged
  2. 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

SituationRecoverable?
Immediately after git checkout HEADYes
After git gcNo
After git gc --prune=nowNo
Weeks later (auto-gc may have run)Check first
Changes never staged with git addNo

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.