You let Claude Code refactor a helper, it rewrote the whole file, you committed, and only then noticed it deleted a 5-line special-case branch that was actually in production. Or you ran git reset --hard against the wrong commit. Or the remote got rebased and your old commits seem to have vanished after git pull. Almost always the code is not gone: the refs no longer point at it, but the git objects still sit in .git/objects. The catch is the clock. An object that is still reachable from your reflog is safe for about 90 days, but a truly dangling object that nothing references is eligible for git gc after only about 2 weeks (the gc.pruneExpire default). So recover first, understand later.
Fastest fix (single file you can name):
git log -p --follow -- path/to/file.ts # find the SHA of the good version
git restore --source=<sha> -- path/to/file.ts
Fastest fix (a whole commit / HEAD disappeared):
git reflog # find the hash from before the bad reset/rebase
git reset --hard <hash> # or: git branch recovered <hash> (non-destructive)
If neither finds it, jump to Path C: git fsck. The rest of this article walks each path and shows how to confirm the recovery worked.
Which path am I on?
| Symptom | Most likely cause | Go to |
|---|---|---|
| One file lost lines, the rest of the repo is fine | A commit (often an AI rewrite) overwrote part of the file | Path A |
Recent commits are simply gone from git log | git reset --hard / git rebase moved HEAD | Path B |
git log shows the commit was never yours after git pull | Branch was rebased and force-pushed by someone | Path B |
| A deleted branch’s work is gone | git branch -D removed an unmerged branch | Path C |
| A dropped or cleared stash | git stash drop / git stash clear | Path C |
| ”It worked two weeks ago, now it doesn’t,” but you don’t know which commit | A regression slipped in somewhere | Path D: bisect |
Common causes
Ordered by how often they actually happen, highest first.
1. An AI or manual commit overwrote part of a file
You ran git add . && git commit -am "..." and an agent’s rewrite blew away lines you needed. This is the most common AI-coding loss, especially when the prompt did not say “only modify function X.”
How to spot it: git log --oneline -5 -- path/to/file.ts shows the file’s recent commits; git diff HEAD~1 -- path/to/file.ts shows exactly what the last commit changed.
2. git reset --hard moved HEAD too far
You meant to undo the last commit and typed git reset --hard HEAD~3 instead. Three commits gone from git log.
How to spot it: the first line of git reflog reads HEAD@{0}: reset: moving to ....
3. Branch was rebased or force-pushed
A teammate (or you) ran git rebase main then git push --force, every old commit hash changed, and after git pull your old commits are orphaned.
How to spot it: after git fetch, the hashes in git log origin/<branch> --oneline no longer match what is in your git reflog.
4. Branch deleted by mistake
git branch -D feature-x removed an unmerged branch, so its tip commit is now dangling.
How to spot it: git reflog show feature-x errors out, but git reflog | grep -i feature-x still shows the last checkout hash.
5. Stash dropped or cleared
git stash pop hit a conflict, you panicked and ran git stash drop without resolving, or you ran git stash clear after stacking several stashes.
How to spot it: git fsck --unreachable | grep commit lists dangling commits; the stash is usually a merge commit, so the timestamp near when you stashed is the giveaway.
Recovery paths
Step 0: Snapshot the current state first
Whatever path you take next, start by making “now” recoverable too:
git stash push -u -m "before-recovery-$(date +%s)"
# or, safer, because it touches nothing:
git branch backup/before-recovery-$(date +%s)
If recovery goes sideways you can git stash pop or git checkout backup/... to get back to this moment.
Step 1: Path A — you know which file lost content
The most common case. Use log -p to read the file’s full history and find the good version:
git log -p --follow -- path/to/file.ts
--follow keeps tracing across renames. Scroll to the version you want and note its SHA. Then restore it:
# Modern, recommended on Git 2.23+ (any current install in 2026):
git restore --source=<sha> -- path/to/file.ts
# Equivalent older form (also stages the file):
git checkout <sha> -- path/to/file.ts
# Or view it without touching the working tree:
git show <sha>:path/to/file.ts > /tmp/oldversion.ts
diff /tmp/oldversion.ts path/to/file.ts
git restore is the command Git now recommends for pulling file contents from a commit; git checkout <sha> -- file still works and additionally stages the result. If you only need a few lines back, copy-paste from the git show output is cleaner than restoring the whole file.
Step 2: Path B — an entire HEAD was reset or rebased away
git reflog is git’s local log of every move HEAD has made:
git reflog
# Example output:
# a3f7c1d HEAD@{0}: reset: moving to HEAD~3
# 9b2e8f4 HEAD@{1}: commit: fix auth bug
# 7c4a1d2 HEAD@{2}: commit: add login form
# ...
Find the hash from before the bad operation (here 9b2e8f4), inspect it, then recover:
git show 9b2e8f4 # confirm it is the right state
git reset --hard 9b2e8f4 # move HEAD fully back
# Safer alternative: branch it off without touching your current HEAD
git branch recovered-state 9b2e8f4
Prefer git branch recovered-state <hash> when you are not certain: it is non-destructive, and you can compare with git diff HEAD recovered-state before committing to a reset --hard.
Step 3: Path C — reflog can’t find it either (deleted branch, dropped stash)
If even reflog does not have it (a deleted branch from a different clone, or an entry already aged out), scan for dangling objects with fsck:
git fsck --lost-found
# Example:
# dangling commit a1b2c3d...
# dangling commit e4f5a6b...
git show a1b2c3d # inspect each candidate
git show e4f5a6b
git branch recovered a1b2c3d # once you find it, give it a ref
Sorting candidates by time makes the right one obvious:
git fsck --unreachable --no-reflogs | grep commit | \
awk '{print $3}' | \
xargs -I{} git log -1 --format='%ci %H %s' {} | sort -r
Recovering a dropped stash is the same idea, but a stash is a merge commit, so filter for those:
git fsck --unreachable | grep commit | cut -d ' ' -f3 | \
xargs git log --merges --no-walk --oneline
# Then re-apply the one you want:
git stash apply <commit-hash>
If you are still in the same terminal, git stash drop printed the hash it dropped, for example Dropped refs/stash@{0} (2ca03e2...). Feed that hash straight into git stash apply.
Step 4: Path D — use git bisect to find the version that still worked
When you only know “it worked two weeks ago, it does not now” but not which commit broke it:
git bisect start
git bisect bad HEAD # current state is broken
git bisect good v1.4.0 # this tag (or hash) was known good
# git checks out a commit halfway between; test it, then answer:
git bisect good # or: git bisect bad
# repeat until git names the first bad commit, then:
git bisect reset
Once bisect names the breaking commit, use Path A to restore the file to its pre-break version. To automate, give bisect a test script: git bisect run npm test (or any command that exits non-zero on failure) and walk away.
Step 5: Give the recovered work a permanent ref before gc removes it
A recovered dangling commit is still unreferenced. git gc prunes loose objects older than the gc.pruneExpire window (default about 2 weeks), and an automatic gc can fire during routine commands. The moment you have recovered something, give it a ref and push it:
git branch recovery/found-it <sha>
git push origin recovery/found-it
A branch (or tag) makes the object reachable, which removes it from gc’s reach entirely.
How to confirm it’s fixed
- File restore (Path A):
git diff(orgit diff --staged) should now show the missing lines reappearing in your working tree. Run the relevant test or open the file to confirm the specific block is back. - HEAD restore (Path B):
git log --oneline -5should list the commits that had disappeared, andgit statusshould be clean (or match what you expect). - Dangling recovery (Path C/D):
git branch --contains <sha>should now print the branch you created, confirming the commit is referenced and safe from gc. - Pushed recovery:
git ls-remote origin recovery/found-itshould return a hash, proving the remote has it.
FAQ
How long do I have before the lost commit is really gone?
If the commit is still in your reflog, about 90 days (gc.reflogExpire). If it is fully dangling with no reflog entry, as little as ~2 weeks (gc.pruneExpire), and an automatic gc can run during normal commands. Treat any loss as urgent and give it a branch immediately.
git reflog shows nothing useful. Now what?
The reflog is per-clone and local. If the work happened in a different clone or CI checkout, your local reflog never recorded it. Go straight to Path C (git fsck --lost-found) on the clone where the work existed.
Does git pull after a force-push destroy my local commits?
Not immediately. A force-pushed rebase changes origin/<branch>, but your old local commits stay in your reflog. Run git reflog to find your pre-pull hash and branch it off before doing anything else.
Can I recover uncommitted changes I never staged?
Only if they were stashed or briefly committed. Plain working-tree edits that were overwritten by git checkout/git restore or git reset --hard and never went into a git object are not recoverable from git. An editor’s Local History (below) is your only chance there.
git restore vs git checkout for an old file?
They both pull a file’s contents from a commit. git restore --source=<sha> -- file is the current recommended syntax (Git 2.23+); git checkout <sha> -- file does the same and also stages the file. Use whichever your muscle memory prefers.
Prevention
- Before any AI refactor, run
git commit -am "checkpoint before AI refactor"so the agent always has a known-good fallback. - Enable your editor’s auto-save and Local History (VS Code and Cursor both have Local Timeline / Local History built in). It is independent of git and catches edits that never became git objects.
git pushto a remote or personal backup branch at the end of the day. The reflog is local only and disappears with the disk.- After a critical change, push to a
wip/branch before continuing, so a later squash can never strip a mid-state commit you might need. - In team policy, block
force pushto shared branches (main/develop) and require--force-with-lease, which refuses to overwrite work it has not seen. - To extend retention beyond the defaults, set
git config gc.reflogExpire 180.days(ornever) andgit config gc.pruneExpire 30.days. The defaults are 90 days for reflog and ~2 weeks for prune, so raising these buys you a bigger recovery window.
Related
- AI rollback changes
- How to inspect AI-generated diffs
- Multiple AI agents created conflicts
- AI pre-commit review workflow
- AI dependency upgrade workflow
Tags: #AI coding #Debug #Troubleshooting