Everything we have seen in this module took one thing for granted: that the object database was healthy. The reflog found the hash, git branch created the pointer, git show read the commit. The objects were where they were supposed to be and their content was correct.
This lesson deals with when it is not.
error: object file .git/objects/4f/8a2e6c9b1d5e3a7f2c8b6d9e4a1f5c3b7d2e9a is empty fatal: loose object 4f8a2e6c9b1d5e3a7f2c8b6d9e4a1f5c3b7d2e9a (stored in .git/objects/4f/8a2e6...) is corrupt
It is the most frightening message in the whole course, and there are two things to start with.
The first is that it is almost never genuine corruption. Most alarming messages have mundane causes: a full disk, an interrupted process, an antivirus that locked a file. Before "repairing" anything, you have to rule those out.
The second is what makes this lesson habitable: in a distributed system, every clone is an almost complete backup. Bruno's repository has the same objects as yours. So does the server. That is, almost always, the fastest recovery route, and often the right answer is simply to clone again rather than trying to fix anything.
We are going to see how to tell real corruption from something else, how to read git fsck without panicking, how to repair the frequent cases in order from least to most serious, and — most importantly — when to stop trying.
Contents
- When to suspect corruption and when it is something else
- The real causes of corruption
git fsckas a diagnostic tool- Repairs, from least to most serious
- The safety net of the distributed model
- Rebuilding a repository while keeping your work
- Prevention: maintenance and proper backups
- When to stop repairing and clone again
- When to suspect corruption and when it is something else
First of all, the elimination table. Many errors that look like corruption are not.
| Message / symptom | Is it corruption? | Real cause and solution |
|---|---|---|
fatal: Unable to create '.git/index.lock': File exists |
No | Orphaned lock (09-01, section 5.5) |
error: insufficient permission for adding an object |
No | Permissions: sudo was used at some point. chown -R |
fatal: detected dubious ownership |
No | Directory ownership (09-01, section 5.6) |
error: cannot open .git/objects/...: No space left on device |
Not yet, but it will be | Disk full. Free up space BEFORE going on |
fatal: bad object HEAD |
Perhaps | Broken HEAD (section 4.5) or an empty repository |
warning: ignoring dangling symref |
No | A symbolic reference to something non-existent. Harmless |
dangling commit / blob / tree in fsck |
No | Completely normal (09-04) |
error: object file ... is empty |
Yes | A 0-byte object file. Section 4.4 |
fatal: loose object ... is corrupt |
Yes | The content does not match its hash. Section 4.4 |
error: refs/heads/x does not point to a valid object! |
Yes | Broken reference. Section 4.3 |
missing blob / broken link from in fsck |
Yes | A necessary object is missing. Section 3 |
fatal: index file smaller than expected |
Yes, but mild | Corrupt index (section 4.1): it is a cache |
error: bad signature 0x00000000 |
Yes, mild | The same: a corrupt index |
fatal: packed object ... cannot be read |
Yes, serious | Damaged packfile. Section 4.4 |
| Everything is slow but works | No | Performance (08-06) |
The preliminary elimination, in three commands
Before touching anything in .git:
If you see Use% 100%, that is the problem. Free up space before going on; any repair is going to write objects and will fail all the same.
# 2. Is the repository mine? Do I have permissions?
ls -ld .git .git/objects
find .git -not -user "$(id -un)" | headIf instead of a local ext4/apfs/ntfs you see nfs, cifs, fuse.sshfs or a path inside Dropbox, OneDrive, iCloud or Google Drive, you already have the explanation, and it is section 2.
- The real causes of corruption
Git is remarkably robust. Objects are immutable, they are written once, and each one carries its own checksum: an object's name is the hash of its content, so any alteration is detected on reading it (lesson 01-04). Corruption, when it happens, almost always comes from outside.
| Cause | How it happens | Frequency |
|---|---|---|
| Repository in a cloud-synced folder | Dropbox/OneDrive/iCloud/Drive synchronise .git files in the middle of a commit, or resolve "conflicts" by duplicating files |
By far the commonest |
| Full disk | Git writes an object halfway and cannot finish it | Very common |
Power cut or forced shutdown during a commit/gc |
Interrupted write | Common on laptops |
.git on a network volume (NFS, SMB, sshfs) |
Unreliable locking and mtime, reordered writes |
Common in corporate environments |
| Antivirus | Locks or quarantines files in .git/objects |
Common on Windows |
| Killing Git processes brutally | kill -9 during gc or repack |
Occasional |
| Disk with bad sectors | Failing hardware | Rare, but serious |
Editing .git files by hand |
"I'll just fix this with a text editor" | Self-inflicted |
The cloud case, which deserves emphasis
Do not put a Git repository inside a cloud-synchronised folder.
It is the classic cause, and the reasoning is simple: Git writes many small files in a specific order (object, then index, then reference). A synchroniser uploads them according to its own criteria and speed. If you work from two machines, the synchroniser can combine a .git/index from one with the references from the other, or create files of the sort main (conflicted copy 2026-07-28).lock.
The result is a repository in a state Git could never have produced.
And the ironic part: there is no need for it at all. Git is already a distributed synchronisation system. To have the project on two machines, the answer is a remote, not Dropbox.
# If you find yourself in this situation: get the repository out of the synced folder
mv ~/Dropbox/task-manager ~/projects/task-manager
cd ~/projects/task-manager
git fsck --full # check the damageIf you genuinely need a repository to live there, the only safe way is for it to be a bare repository (--bare) used as a remote, which is never written to from two places at once. Even so, git bundle (section 7) is a better idea.
The antivirus check on Windows
If Carla works on Windows 11 and suffers intermittent errors:
And in Git for Windows, a setting that reduces problems with antivirus software and with slow filesystems:
git fsck as a diagnostic tool
git fsck as a diagnostic toolgit fsck (file system check) walks the object database, verifies that each object's content matches its hash, and checks that all the references between objects resolve.
Useful options:
| Option | What it does |
|---|---|
--full |
Also checks the objects inside packfiles (by default only the loose ones) |
--no-progress |
No progress bar: better for redirecting to a file |
--lost-found |
Creates links in .git/lost-found/ (09-04) |
--unreachable |
Also lists the unreachable, taking the reflog into account |
--connectivity-only |
Fast: only checks links, does not verify hashes |
--strict |
Stricter: warns about things Git tolerates |
--dangling / --no-dangling |
Show or hide the dangling ones |
Reading the output: the normal versus the serious
This is the table that avoids unnecessary panic.
fsck line |
Severity | What it means | What to do |
|---|---|---|---|
dangling commit <sha> |
None | A commit no reference reaches | Nothing. It is normal after reset, rebase, --amend |
dangling blob <sha> |
None | The content of an uncommitted git add |
Nothing (or recover it, 09-04) |
dangling tree <sha> |
None | A directory with no commit using it | Nothing |
unreachable <type> <sha> |
None | The same, taking the reflog into account | Nothing |
notice: HEAD points to an unborn branch |
None | A freshly created repository with no commits | Nothing |
warning: ... has zero-padded file modes |
Very low | An old or imported repository | Nothing, or fsck.zeroPaddedFilemode ignore |
warning: ... missingSpaceBeforeDate |
Very low | Malformed metadata from an import | Nothing |
error: <sha>: object corrupt or missing |
HIGH | The object cannot be read | Section 4.4 |
missing blob <sha> |
HIGH | A tree references a blob that does not exist | Section 4.4 |
missing tree <sha> |
HIGH | A commit references a non-existent tree | Section 4.4 |
broken link from <sha> to <sha> |
HIGH | An object points at another that is missing | Section 4.4 |
error: refs/heads/x does not point to a valid object! |
HIGH | Broken reference | Section 4.3 |
dangling commit in enormous quantities |
Medium | May indicate an interrupted gc |
git gc |
The golden rule for reading fsck:
# Hide the normal noise and keep ONLY what matters
git fsck --full --no-progress 2>&1 | grep -v "^dangling" | grep -v "^notice"If that returns nothing, your repository is healthy. Full stop. Everything you saw was noise.
# And to separate by severity
git fsck --full --no-progress 2>&1 | grep -E "^(error|missing|broken)"An example of healthy output
Checking object directories: 100% (256/256), done. Checking objects: 100% (18432/18432), done. dangling commit 9c4e7b2e4a7f3c8b6d2e9a5f1c7b3d8e4a6f2c9b dangling blob 6f2b9d4a8c1e5f3b7d9a2c6e4f8b1d3a5c7e9f2b dangling commit 3e7f1a8b6d2e9a5f1c7b3d8e4a6f2c9b7d3a8f4a
Three apparently alarming lines, zero problems. They are the remains of last week's reset --hard and of a git add that never got committed. An active repository always has this.
An example of serious output
Checking object directories: 100% (256/256), done.
error: refs/heads/GT-241 does not point to a valid object!
error: object file .git/objects/4f/8a2e6c9b1d5e3a7f2c8b6d9e4a1f5c3b7d2e9a is empty
fatal: loose object 4f8a2e6c9b1d5e3a7f2c8b6d9e4a1f5c3b7d2e9a is corrupt
missing blob 8a1f6c3d5e2b9c7f4a6d1e8b3c5f7a9d2e4b6c8a
broken link from tree 2f8c6e1a4b7d9c3e5f2a8b6d1c9e4f7a3b5d2c8e
to blob 8a1f6c3d5e2b9c7f4a6d1e8b3c5f7a9d2e4b6c8aHere there is real work to do. A broken reference and an unreadable object, with the tree that needs it identified. That is section 4.
Checking one particular object
That message, for a hash you know existed, is confirmation that the object has disappeared.
- Repairs, from least to most serious
4.1. A corrupt .git/index (the most frequent and mildest case)
or
You have lost nothing. And the reason is conceptually important:
The index is a derived cache. It contains no unique information: it is a snapshot of what should go into the next commit, entirely reconstructible from
HEADand the working directory.
That is why the repair is one of the most rewarding in Git:
# 1. Remove the broken index
rm -f .git/index
# 2. Rebuild it from HEAD
git reset
# 3. Check
git statusThe only thing lost is what was staged with git add: after the rebuild, all your modifications appear as unstaged. The changes themselves are intact, because they live in the files on disk, not in the index.
An important nuance: use git reset (--mixed), never git reset --hard. The first rebuilds the index and leaves the working directory alone; the second would sweep away all your modifications (lesson 09-02).
If you also had a merge conflict half done, the information from the index stages (:1:, :2:, :3: from lesson 03-05) is indeed lost, and the merge has to be redone:
4.2. Orphaned locks
Already seen in lesson 09-01, here in its complete version. Locks are .lock files that Git creates before writing and deletes when it finishes:
The procedure:
# 1. ALWAYS first: is any Git running?
ps aux | grep "[g]it "
# 2. How old are they?
ls -l $(find .git -name "*.lock")
# 3. If there are no processes and they are old, delete them
find .git -name "*.lock" -delete
# 4. Check
git statusA special case that deserves care: .git/config.lock. If Git died while writing the configuration, the .git/config file may have been left truncated. Check it before deleting the lock:
If it is broken, the local configuration is the easiest thing in the whole repository to redo by hand.
4.3. Broken references
What it is. The file refs/heads/GT-241 contains a hash that does not correspond to any existing object. Remember that a branch is 41 bytes of text (lesson 03-01):
Confirmed: the reference points at nothing.
Repair A: the branch's reflog. It is the best option, because it preserves history:
b52c9d1 GT-241@{0}: commit: GT-241 indicator styles
7d3a8f4 GT-241@{1}: commit: GT-241 calculate the counter
4f8a2e6 GT-241@{2}: branch: Created from mainTry the entries from the top down until you find a valid one:
git cat-file -t 7d3a8f4 # commit → this one does exist
git update-ref refs/heads/GT-241 7d3a8f4
git log --oneline GT-241 -3You have lost the last commit, but you get the branch back. Far better than nothing.
Repair B: packed-refs. References also live consolidated in a file (lesson 08-06):
Sometimes the packed version is valid even though the loose one is broken. If you delete the loose one, Git uses the packed one:
Repair C: the remote.
Repair D: if the branch does not matter, delete it.
And the bulk clean-up, when there are many broken references:
# See all the references and whether they are valid
git for-each-ref --format='%(refname) %(objectname)' | while read -r ref sha; do
git cat-file -e "$sha" 2>/dev/null || echo "BROKEN: $ref -> $sha"
done4.4. Unreadable or missing loose objects
This is the serious case.
error: object file .git/objects/4f/8a2e6c9b1d5e3a7f2c8b6d9e4a1f5c3b7d2e9a is empty fatal: loose object 4f8a2e6... is corrupt
First step, the exact diagnosis:
Zero bytes: an interrupted write. It is the typical pattern of a full disk or a power cut.
# What was it supposed to be? (it can often be deduced from the fsck context)
git fsck --full --no-progress 2>&1 | grep -A2 "4f8a2e6"broken link from tree 2f8c6e1a4b7d9c3e5f2a8b6d1c9e4f7a3b5d2c8e
to blob 4f8a2e6c9b1d5e3a7f2c8b6d9e4a1f5c3b7d2e9aThe repair that almost always works: bring it from another clone.
This is the central idea of the lesson and section 5 develops it. Git objects are identical everywhere: the same content produces the same hash and the same compressed file. If Bruno has that commit, his object serves exactly the same purpose.
# 1. Remove the broken object (it is zero bytes: there is nothing to lose!)
rm .git/objects/4f/8a2e6c9b1d5e3a7f2c8b6d9e4a1f5c3b7d2e9a
# 2. Bring it from the remote: the simple way
git fetch origin --force '+refs/heads/*:refs/remotes/origin/*'
# 3. Or copy it directly from another clone
cp /path/to/brunos/clone/.git/objects/4f/8a2e6c9b1d5e3a... \
.git/objects/4f/
# 4. Check
git fsck --full --no-progress 2>&1 | grep -E "^(error|missing|broken)"If the other clone has it packed rather than loose, it has to be extracted:
# In the healthy clone:
cd /path/to/brunos/clone
git cat-file -p 4f8a2e6 > /tmp/recovered-object
# In the broken repository:
cd ~/projects/task-manager
git hash-object -w -t blob /tmp/recovered-objectIf the resulting hash matches the one that was missing, the repair is exact. And it cannot fail to match: the hash is calculated from the content, so if a different one comes out, the content was not the same (lesson 01-04).
For an object of type tree or commit, the same procedure with -t tree / -t commit and the exact content that cat-file -p gives in the healthy clone.
If the object was a damaged packfile:
A broken packfile is worse, because it contains thousands of objects and the delta chains depend on one another (lesson 08-06). You can try to extract what is salvageable:
# Move the broken pack out of the way and see what survives
mkdir -p /tmp/broken-packs
mv .git/objects/pack/pack-a1b2c3.* /tmp/broken-packs/
git fsck --full --no-progress
# Try to recover individual objects from the broken pack
git verify-pack -v /tmp/broken-packs/pack-a1b2c3.idx 2>/dev/null | headBut let us be clear: with a damaged packfile, the right answer is almost always to re-clone (section 8). Extracting objects from a broken pack is an exercise in archaeology that rarely pays off when there is a healthy clone one git clone away.
4.5. A broken HEAD
HEAD is a one-line text file (lesson 03-01):
What it should contain:
What may have happened:
Content of .git/HEAD |
Problem | Repair |
|---|---|---|
ref: refs/heads/deleted-branch |
The branch does not exist | git symbolic-ref HEAD refs/heads/main |
| Empty or binary rubbish | Interrupted write | Rewrite it |
A bare hash (with no ref:) |
Detached HEAD: not an error | git switch main |
| A hash of a non-existent object | Real corruption | Point it at a valid branch |
# See which branches actually exist
ls .git/refs/heads/
cat .git/packed-refs 2>/dev/null | grep refs/heads
# Repair by pointing at an existing branch (the correct way, not editing by hand)
git symbolic-ref HEAD refs/heads/main
# Check
git status
git log --oneline -3If no valid branch remains, the HEAD reflog is still plain, readable text even when Git does not work:
4.6. A corrupt .git/config
It is the easiest file to repair, because its content is trivial to redo:
[core]
repositoryformatversion = 0
filemode = true
bare = false
[remote "origin"]
url = git.example.com:team/task-manager.git
fetch = +refs/heads/*:refs/remotes/origin/*
[branch "main"]
remote = origin
merge = refs/heads/mainIf it is truncated, edit it (here you may, with a text editor: config is plain text by design and Git does not verify it with hashes) or rebuild the essentials:
git remote add origin git.example.com:team/task-manager.git
git branch --set-upstream-to=origin/main main
git config user.name "Ana Ferrer"
git config user.email "[email protected]"Summary table of repairs
| Problem | Severity | Repair | Is anything lost? |
|---|---|---|---|
Corrupt .git/index |
Low | rm .git/index && git reset |
Only what was staged |
Orphaned .lock |
Low | Check processes and delete | Nothing |
Broken .git/config |
Low | Edit or redo with git config |
The local configuration |
| Broken reference | Medium | Reflog, packed-refs, or the remote |
Perhaps that branch's last commits |
Broken HEAD |
Medium | git symbolic-ref HEAD refs/heads/main |
Nothing |
| Unreadable loose object | High | Bring it from another clone | Nothing, if somebody has it |
| Damaged packfile | Very high | Re-clone | Only what was exclusively local |
| Widespread corruption | Very high | Re-clone | The same |
- The safety net of the distributed model
Here is the idea that makes this lesson manageable, and that closes the circle opened in module 1.
In a centralised system, the server is a single point of failure: if it becomes corrupted, the project has become corrupted. In Git, each clone contains the entire history. Ana, Bruno, Carla, Diego and the server have five complete copies of the object database.
flowchart TD
S["git.example.com<br/>complete history"]
A["Ana (Ubuntu)<br/>complete history<br/>+ unpublished GT-241"]
B["Bruno (macOS)<br/>complete history"]
C["Carla (Windows)<br/>complete history"]
D["Diego (fork)<br/>complete history"]
S <--> A
S <--> B
S <--> C
S <--> D
Practical consequence: when your repository becomes corrupted, the right question is not "how do I repair it?" but "what do I have that nobody else has?".
# 1. Which branches have I not published?
git for-each-ref --format='%(refname:short) upstream=[%(upstream:short)]' refs/headsGT-241 has no upstream: it exists only on this machine. It is the only thing that needs saving.
# 2. Which commits do I have ahead of the remote?
git log --oneline origin/main..main
git log --oneline --branches --not --remotes
# 3. Are there any stashes?
git stash list
# 4. Are there any uncommitted changes?
git status --shortWith those four answers you already know exactly what the repair is worth. If the answer is "nothing, it is all published", re-cloning takes two minutes and is the perfect solution.
And if the problem is with the server, the direction is reversed: any clone in the team can rebuild it.
# Rebuild the remote from a healthy clone
cd ~/projects/task-manager
git push --all origin
git push --tags origin
# Or create a complete mirror from scratch
git clone --mirror ~/projects/task-manager /tmp/task-manager-restored.git--mirror copies all the references as they are: branches, tags, notes. It is the correct way to duplicate a bare repository.
- Rebuilding a repository while keeping your work
The complete procedure, for when you have decided to re-clone but have local material to save.
Step 1: the backup, always
Even if you are going to re-clone. It costs ten seconds and it is the only thing that stops a mistake during the rescue being final.
Step 2: an inventory of what only you have
cd ~/projects/task-manager
# Unpublished branches
git for-each-ref --format='%(refname:short) %(upstream)' refs/heads | awk '$2==""{print $1}'
# Local commits ahead of the remote, across all branches
git log --oneline --branches --not --remotes
# Stashes
git stash list
# Uncommitted changes
git status --short
# Unpublished local tags
git tag --no-merged origin/main 2>/dev/nullStep 3: extract the local material to a bundle
git bundle packs commits into a single file that works as a remote. It is the right tool for this, and it works even if the repository is partially damaged (as long as the objects involved are healthy).
# Everything I have that is not on the remote
git bundle create /tmp/rescue.bundle --branches --not --remotes
# Or just one particular branch
git bundle create /tmp/GT-241.bundle GT-241
# Check that the bundle is valid
git bundle verify /tmp/rescue.bundleThe bundle contains these 2 refs: 7d3a8f4a2c6e9b1d5f3a8c6e2b9d4f7a1c5e8b3d refs/heads/GT-241 b52c9d1e4a7f3c8b6d2e9a5f1c7b3d8e4a6f2c9b refs/heads/GT-238 The bundle requires these 1 ref: 4f8a2e6c9b1d5e3a7f2c8b6d9e4a1f5c3b7d2e9a refs/heads/main /tmp/rescue.bundle is okay
If you also have uncommitted changes, take those out separately:
git diff > /tmp/uncommitted-changes.patch
git diff --cached > /tmp/staged-changes.patch
# And the untracked files, by hand:
cp -a draft.txt notes.md /tmp/rescue-files/If git bundle fails because the repository is too badly damaged, the brute-force route also works:
# Copy the project's files (without .git) and treat them as new changes
mkdir /tmp/rescue-work
rsync -a --exclude='.git' ~/projects/task-manager/ /tmp/rescue-work/You lose the local branches' history, but you keep the content, which is usually what mattered.
Step 4: clone again
cd ~/projects
mv task-manager task-manager-broken
git clone git.example.com:team/task-manager.git
cd task-managerStep 5: bring the rescued material back in
# Fetch the bundle as if it were a remote
git fetch /tmp/rescue.bundle 'refs/heads/*:refs/heads/*'
git branchThe local branches are back, with all their commits.
# The uncommitted changes
git apply /tmp/uncommitted-changes.patch
# The ones that were staged
git apply --cached /tmp/staged-changes.patch
# The untracked files
cp -a /tmp/rescue-files/* .Step 6: verify and clean up
git fsck --full --no-progress 2>&1 | grep -E "^(error|missing|broken)"
git log --oneline --all --graph -15
git statusIf there are no errors and all your work is there:
Do not delete the copy until you have verified. It is the only rule that matters in this section.
The procedure in a table
| Step | Command | What for |
|---|---|---|
| 1 | cp -a <repo> <repo>-BROKEN-<date> |
Safety net |
| 2 | git for-each-ref + git log --branches --not --remotes |
Know what is exclusively yours |
| 3 | git bundle create /tmp/rescue.bundle --branches --not --remotes |
Pack up the local material |
| 4 | git clone <url> |
A clean repository |
| 5 | git fetch /tmp/rescue.bundle 'refs/heads/*:refs/heads/*' |
Put the local material back |
| 6 | git fsck --full + checking |
Verify before deleting anything |
- Prevention: maintenance and proper backups
Periodic git maintenance and git fsck
Picking up from lesson 08-06:
As well as improving performance, it keeps the object database tidy and reduces the number of loose files exposed to interrupted writes.
And a periodic check, which in a normal repository takes seconds:
# Quick: links only, does not verify hashes
git fsck --connectivity-only --no-progress
# Complete: verifies each object against its hash. Monthly is fine
git fsck --full --no-progress 2>&1 | grep -vE "^(dangling|notice)"And a configuration that makes Git verify objects as it receives them, catching corruption at the time instead of months later:
git config --global transfer.fsckObjects true
git config --global fetch.fsckObjects true
git config --global receive.fsckObjects trueIt costs a little time on each fetch and in exchange it prevents a malformed object entering your repository. On the server, receive.fsckObjects is downright mandatory.
git bundle: the proper backup
A bundle is a single file containing whatever you ask for, which works as a remote. It is portable, verifiable and does not depend on any service.
# A complete copy of the repository
git bundle create ~/backups/task-manager-$(date +%Y%m%d).bundle --all
# Check
git bundle verify ~/backups/task-manager-20260731.bundle
# Restore: it is cloned as if it were a URL
git clone ~/backups/task-manager-20260731.bundle task-manager-restoredAnd incremental copies, if the repository is large:
A weekly backup script:
#!/usr/bin/env bash
# git-backup.sh — backup of the working repositories
DEST=~/backups/git
mkdir -p "$DEST"
for repo in ~/projects/*/; do
[ -d "$repo/.git" ] || continue
name=$(basename "$repo")
echo "=== $name ==="
git -C "$repo" bundle create "$DEST/$name-$(date +%Y%m%d).bundle" --all \
&& git -C "$repo" bundle verify "$DEST/$name-$(date +%Y%m%d).bundle" > /dev/null \
&& echo " OK" || echo " FAILED"
done
# Keep only the 8 most recent copies of each repository
find "$DEST" -name "*.bundle" -mtime +56 -deleteWhy a bundle is better than copying .git with cp:
cp -a .git |
git bundle |
|
|---|---|---|
| Consistency if a Git is writing | Can copy a half-finished state | Consistent: Git generates it |
| Verifiable | No | Yes: git bundle verify |
| Size | Everything, orphaned objects included | Only what is reachable, compressed |
| Restoring | Copy it back | git clone <file> |
| Portable (email, USB) | Thousands of files | One file |
The prevention list
1. Never a repository in a cloud-synchronised folder. The number one cause. Use a remote.
2. Never .git on a network volume if you can avoid it. Clone locally and push.
3. Exclude your project folders from the antivirus on Windows.
4. Keep an eye on disk space. A df -h now and then.
5. git maintenance start in your working repositories.
6. transfer.fsckObjects true to detect corruption as it comes in.
7. Publish your branches. A published branch is on two machines. It is the best backup and the cheapest.
8. A weekly git bundle of what only you have.
9. Do not kill Git processes with kill -9, especially during gc or repack.
10. Do not edit .git files by hand (apart from config, which is text by design). Use git update-ref, git symbolic-ref, git config.
- When to stop repairing and clone again
The most important decision in the lesson, and the one that saves the most time.
Re-cloning is the right answer when:
- The repository is completely published (nothing local unpushed). Then re-cloning costs nothing at all.
- There is a damaged packfile. Extracting objects from a broken pack rarely pays off.
git fsckreturns more than a handful of errors.- The corruption comes back after you repair it: there is an underlying cause (disk, cloud, antivirus) you have not resolved.
- You have spent more than thirty minutes trying to repair it. The cost of a
git cloneis minutes; that of an afternoon of archaeology is not.
It is worth attempting the repair when:
- The problem is the index, a lock,
HEADorconfig. It is minutes and nothing is lost. - It is one broken reference and the reflog or
packed-refsresolves it. - One object is missing and you know where to get it.
- You have important unpublished local work and need to extract it before re-cloning (section 6).
In tree form:
flowchart TD
Q1{"Do I have unpublished<br/>local work?"}
Q1 -->|No| R1["RE-CLONE.<br/>It is the right answer<br/>and it takes two minutes"]
Q1 -->|Yes| Q2{"Is the damage the index, a lock,<br/>HEAD or a reference?"}
Q2 -->|Yes| R2["Repair: section 4.<br/>Minutes, no loss"]
Q2 -->|No: objects or packfiles| Q3{"Can I extract the local material<br/>with git bundle?"}
Q3 -->|Yes| R3["Bundle + re-clone +<br/>bring it back (section 6)"]
Q3 -->|No| R4["rsync the content without .git,<br/>re-clone, and reapply as changes"]
And the sentence that sums up the lesson:
A damaged Git repository is not a data problem: it is a logistics problem. The data is almost always somewhere else. Your job consists of identifying what is exclusively yours, saving it, and bringing the rest from wherever it is healthy.
Common Mistakes and Tips
Mistake 1: being alarmed by git fsck's dangling entries. They are completely normal in any active repository. Filter with grep -v "^dangling" and see whether anything is left.
Mistake 2: writing off as corruption what is a full disk. df -h is the first command. Repairing with the disk at 100 % cannot work.
Mistake 3: keeping the repository in Dropbox, OneDrive or iCloud. It is the number one cause of real corruption, and there is no need for it at all: Git already synchronises.
Mistake 4: git reset --hard to repair a corrupt index. rm .git/index && git reset rebuilds it without touching your files; with --hard you would lose them all.
Mistake 5: editing .git files with a text editor. Apart from config, use the commands: git update-ref, git symbolic-ref, git config.
Mistake 6: repairing without having made a copy. cp -a costs ten seconds and stops a mistake during the rescue being final.
Mistake 7: spending hours repairing a repository that is entirely published. Check first what you have that nobody else has. If the answer is "nothing", re-clone.
Mistake 8: using cp -a .git as the "official" backup. It will do in an emergency, but it can copy a half-finished state. git bundle is consistent and verifiable.
Mistake 9: deleting the broken repository before verifying the new one. Verify with git fsck and check that all your work is there; then delete.
Tip 1: learn the fsck filter. git fsck --full --no-progress 2>&1 | grep -vE "^(dangling|notice)". If it returns nothing, you are healthy.
Tip 2: a weekly git bundle create ... --all. One file, verifiable, restorable with git clone.
Tip 3: transfer.fsckObjects true in the global configuration. It detects corruption as it comes in, not months later.
Tip 4: publish your branches. It is the cheapest backup and the one that saves the day most often.
Tip 5: the index is a cache. Losing it does not matter; remembering that avoids a lot of unnecessary panic.
Tip 6: set yourself a time limit. Thirty minutes of repairing and, if it is still broken, re-clone. The clone takes two.
Exercises
Exercise 1: reading git fsck without panicking
- Create a repository with ten commits, a branch with three commits of its own and an uncommitted
git add. - Produce dangling objects: a
reset --hard HEAD~3, acommit --amendand abranch -D. - Run
git fsck --fulland count how many lines it returns. - Apply the filter from tip 1 and check that nothing is left. Explain why the repository is healthy despite the earlier lines.
- Identify, among the dangling ones, which corresponds to the uncommitted
git addand recover it withgit cat-file -p.
Exercise 2: breaking and repairing
On a test repository (never on a real one):
- Corrupt the index:
printf 'rubbish' > .git/index. Rungit statusand note the error. Repair it and explain why nothing is lost. - Break a reference:
echo "0000000000000000000000000000000000000000" > .git/refs/heads/GT-241. Rungit fsckand repair it with the reflog. - Break
HEAD:echo "ref: refs/heads/nonexistent" > .git/HEAD. Diagnose and repair withgit symbolic-ref. - Empty a loose object: locate one with
find .git/objects -type fand truncate it with: > <file>. Rungit fsck --fulland observe the error. - Repair point 4 from a healthy clone that you will have made before breaking anything.
- After each repair, run a filtered
git fsck --fulland confirm that it is clean.
Exercise 3: a complete rescue with bundle
- Set up a local remote with
git init --bareand clone it. - In the clone, publish two commits on
mainand create two unpublished local branches with three commits each. Add a stash and some uncommitted change. - Do the inventory from section 5: which branches have no upstream, which commits are not on the remote.
- Create a bundle with all the local material and verify it.
- Delete the whole clone (simulating irreparable corruption) and clone again.
- Bring the two branches back from the bundle and check that the six commits are there.
- Explain what has been lost and what has not, and how you would have avoided it by publishing the branches.
Solutions
Solution 1:
rm -rf /tmp/p9-05 && mkdir /tmp/p9-05 && cd /tmp/p9-05 && git init -q -b main
git config user.name "Ana Ferrer"; git config user.email "[email protected]"
for i in $(seq 1 10); do echo "line $i" >> app.js; git add .; git commit -q -m "commit $i"; done
git switch -q -c GT-241
for i in 1 2 3; do echo "filter $i" >> filter.js; git add .; git commit -q -m "GT-241 part $i"; done
echo "STAGED BUT NOT COMMITTED" > styles.css && git add styles.css# 2. The disasters
git reset -q --hard HEAD~3
git commit -q --amend -m "commit 10 (message changed)" 2>/dev/null || true
git switch -q main
git branch -D GT-241# 3. The complete output
git fsck --full --no-progress 2>&1 | wc -l
git fsck --full --no-progress 2>&1 | headChecking object directories: 100% (256/256), done. Checking objects: 100% (43/43), done. dangling commit 9c4e7b2e4a7f3c8b6d2e9a5f1c7b3d8e4a6f2c9b dangling blob 6f2b9d4a8c1e5f3b7d9a2c6e4f8b1d3a5c7e9f2b dangling commit 3e7f1a8b6d2e9a5f1c7b3d8e4a6f2c9b7d3a8f4a dangling tree 8a1f6c3d5e2b9c7f4a6d1e8b3c5f7a9d2e4b6c8a dangling commit 8f4c2a9b6d2e9a5f1c7b3d8e4a6f2c9b7d3a8f4a
Nine worrying-looking lines.
Zero errors. The repository is perfectly healthy.
The reason: dangling means "this object exists and is readable, but no reference reaches it". It is exactly the expected result of a reset, an --amend and a branch -D: the commits are still there (which is why they are recoverable, lesson 09-04) but they no longer hang off any branch. It is the opposite of a problem: it is the proof that the safety net works.
Real errors start with error:, missing or broken link, and there are none here.
# 5. The blob from the uncommitted add
for b in $(git fsck --lost-found --no-progress 2>/dev/null | awk '/dangling blob/ {print $3}'); do
echo "--- $b ---"; git cat-file -p "$b" | head -2
doneSolution 2:
rm -rf /tmp/p9-05b && mkdir /tmp/p9-05b && cd /tmp/p9-05b && git init -q -b main
git config user.name "Bruno Salas"; git config user.email "[email protected]"
for i in 1 2 3 4 5; do echo "l $i" >> app.js; git add .; git commit -q -m "c$i"; done
git switch -q -c GT-241
echo "filter" > filter.js && git add . && git commit -q -m "GT-241 filter"
git switch -q main
# The healthy clone, BEFORE breaking anything (step 5)
git clone -q /tmp/p9-05b /tmp/p9-05b-healthyNothing lost. The modification was still in the file on disk; the index was only the snapshot of what was going into the next commit, and it is rebuilt from HEAD plus the working directory. It is a cache.
# 2. Broken reference
git rev-parse GT-241 > /tmp/good-hash.txt
echo "0000000000000000000000000000000000000000" > .git/refs/heads/GT-241
git fsck --full --no-progress 2>&1 | grep -E "^(error|missing|broken)"git cat-file -t b52c9d1 # check that the object exists
git update-ref refs/heads/GT-241 b52c9d1
git log --oneline GT-241 -2Repaired, complete with its commit. The reflog kept the correct hash because the reflog file is independent of the reference file.
Git does not fail, but it believes it is on a branch that does not exist: git log shows nothing and a commit would create a new branch.
ls .git/refs/heads/
git symbolic-ref HEAD refs/heads/main
git status -sb | head -1
git log --oneline -1# 4. An emptied loose object
git gc -q --prune=now 2>/dev/null # pack things up to leave few loose objects
echo "new content" > new.txt && git add . && git commit -q -m "c6"
OBJ=$(find .git/objects -type f -path "*/??/*" | head -1)
echo "Chosen object: $OBJ"
cp "$OBJ" /tmp/original-object # just in case
: > "$OBJ"
ls -l "$OBJ"
git fsck --full --no-progress 2>&1 | grep -vE "^(dangling|Checking)"error: object file .git/objects/6f/2b9d4a8c1e5f3b7d9a2c6e4f8b1d3a5c7e9f2b is empty error: unable to mmap .git/objects/6f/2b9d4...: No such device fatal: loose object 6f2b9d4a8c1e5f3b7d9a2c6e4f8b1d3a5c7e9f2b is corrupt
Git refuses to operate: it cannot read an object it needs.
# 5. Repair from the healthy clone
SHA=$(basename $(dirname "$OBJ"))$(basename "$OBJ")
echo "Missing: $SHA"
rm "$OBJ"
# Does the healthy clone have it?
git -C /tmp/p9-05b-healthy cat-file -t "$SHA"# Extract it and write it back
git -C /tmp/p9-05b-healthy cat-file -p "$SHA" > /tmp/content
NEW=$(git hash-object -w -t blob /tmp/content)
echo "Written: $NEW"
[ "$NEW" = "$SHA" ] && echo "MATCH: exact repair"The hash matches, so the repair is demonstrably exact. It cannot be otherwise: an object's name is the hash of its content (lesson 01-04), so if the same hash comes out, the content is identical byte for byte. It is this property that makes repairing a Git repository from another clone safe and verifiable.
# 6. Final verification
git fsck --full --no-progress 2>&1 | grep -vE "^(dangling|notice|Checking)"
git log --oneline --all -3Solution 3:
rm -rf /tmp/p9-05c && mkdir /tmp/p9-05c && cd /tmp/p9-05c
git init -q --bare server.git
git clone -q server.git work && cd work
git config user.name "Carla Vidal"; git config user.email "[email protected]"
# 2. The published material
echo "// task-manager" > app.js && git add . && git commit -q -m "chore: start"
echo "// list" >> app.js && git commit -q -am "feat: task list"
git push -q -u origin main
# Two local branches WITHOUT publishing
for r in GT-241 GT-247; do
git switch -q -c "$r" main
for i in 1 2 3; do echo "$r part $i" >> "$r.js"; git add .; git commit -q -m "$r part $i"; done
done
git switch -q main
# A stash and uncommitted changes
echo "experiment" >> app.js && git stash push -q -m "half-finished experiment"
echo "work in progress" >> app.js
echo "draft" > draft.txt# 3. Inventory
echo "--- Branches with no upstream ---"
git for-each-ref --format='%(refname:short) %(upstream)' refs/heads | awk '$2==""{print " "$1}'
echo "--- Commits that are not on any remote ---"
git log --oneline --branches --not --remotes
echo "--- Stashes ---"
git stash list
echo "--- Uncommitted ---"
git status --short--- Branches with no upstream ---
GT-241
GT-247
--- Commits that are not on any remote ---
2f8c6e1 GT-247 part 3
7a4d9b3 GT-247 part 2
5c1e8f2 GT-247 part 1
9c4e7b2 GT-241 part 3
3e7f1a8 GT-241 part 2
8a1f6c3 GT-241 part 1
--- Stashes ---
stash@{0}: On main: half-finished experiment
--- Uncommitted ---
M app.js
?? draft.txtSix commits, two branches, a stash and two changes exist solely on this machine. That is exactly what the repair is worth.
# 4. The bundle
git bundle create /tmp/rescue.bundle --branches --not --remotes
git bundle verify /tmp/rescue.bundleThe bundle contains these 2 refs: 9c4e7b2... refs/heads/GT-241 2f8c6e1... refs/heads/GT-247 The bundle requires these 1 ref: b52c9d1... /tmp/rescue.bundle is okay
# The stash and the uncommitted material, separately
git stash show -p stash@{0} > /tmp/stash.patch
git diff > /tmp/uncommitted.patch
mkdir -p /tmp/new-files && cp draft.txt /tmp/new-files/# 5. The catastrophe
cd /tmp/p9-05c && rm -rf work
git clone -q server.git work && cd work
git log --oneline --allOnly the published material.
# 6. Bringing it back
git fetch -q /tmp/rescue.bundle 'refs/heads/*:refs/heads/*'
git branch
git log --oneline GT-241 -3
git log --oneline GT-247 -3The six commits and the two branches, with their original hashes.
git apply /tmp/uncommitted.patch
cp /tmp/new-files/draft.txt .
git stash apply --index 2>/dev/null || git apply /tmp/stash.patch
git status --shortWhat has been lost:
- The stash stack as such. The content has been reapplied from the patch, but the
stash@{0}entry does not exist. (A bundle with--allwould have includedrefs/stash.) - The whole reflog, which is local and does not travel (lesson 09-04). The new clone has three entries.
- The old tracking branches and the repository's local configuration.
What has not been lost: not a single commit, not a single change.
How it would have been avoided: by publishing the branches.
With both branches published, the inventory in step 3 would have come out empty, and this whole lesson would have been reduced to rm -rf work && git clone. Two seconds of push against twenty minutes of rescue.
Conclusion
Corruption of a Git repository is far more frightening than it is costly.
- Most alarming messages are not corruption: an orphaned lock, a full disk, a permissions or ownership problem. Rule that out first with
df -handls -ld .git. - The real causes almost always come from outside Git: a repository in a cloud-synced folder (the number one), a full disk, a power cut, a network volume, an antivirus. Git objects are immutable and carry their own verification; Git rarely breaks by itself.
git fsckis read by filtering out the noise. Dangling objects are completely normal — they are the remains of everyreset,rebaseand--amend— and their presence is proof that the safety net from lesson 09-04 works. The serious stuff starts witherror:,missingorbroken link.- The repairs, from least to most serious: the index is a derived cache and is rebuilt with
rm .git/index && git resetwithout losing anything; locks are deleted after checking processes; a broken reference is repaired with the reflog,packed-refsor the remote;HEADwithgit symbolic-ref; an unreadable object by bringing it from another clone, with the guarantee that if the hash matches, the repair is exact. - The distributed model is the safety net. Every clone in the team is an almost complete copy. The right question when faced with a damaged repository is not "how do I repair it?" but "what do I have that nobody else has?", and it is answered with
git for-each-refandgit log --branches --not --remotes. - The rescue is done with
git bundle: pack the local material into a file, re-clone, and bring it back withgit fetch <bundle>. And always with a priorcp -aand a subsequent verification before deleting anything. - Prevention: no repositories in the cloud or on network volumes,
git maintenance start,transfer.fsckObjects true, a periodicgit bundleand — what pays off most — publishing your branches. - And the criterion: if everything is published, re-clone. It takes two minutes and it is the right answer. Save the repairing for the index, the locks,
HEADand the references, or for extracting local work before re-cloning.
With this, the module has covered the four problems announced at the close of module 8: the unfortunate reset --hard, the tangle with the remote, the deleted branch and the repository that refuses to work.
What remains is the cross-cutting part. Because very often the problem is neither that Git is broken nor that you have lost something, but that Git is doing something you do not understand: a file that is ignored when it should not be, a configuration coming from somewhere you did not know existed, a push that fails for opaque reasons, a change of behaviour nobody remembers introducing. For that there is a toolbox of its own — traces, plumbing, check-ignore, check-attr, ls-files — and, above all, a method.
Continue in lesson 09-06: Advanced Debugging Techniques.
Mastering Git: From Beginner to Advanced
Module 1: Introduction to Git
- What Is Git?
- Installing Git
- Basic Git Terminology
- The Git Data Model
- Configuring Git
- Initial Configuration
Module 2: Basic Git Operations
- Creating a Repository
- Cloning a Repository
- The Basic Git Workflow
- Staging and Committing Changes
- Inspecting Changes with git diff
- Viewing Commit History
Module 3: Branching and Merging
- Understanding Branches
- Creating and Switching Branches
- Merging Branches
- Merge Strategies
- Resolving Merge Conflicts
- Branch Management
Module 4: Working with Remote Repositories
- Understanding Remote Repositories
- Adding a Remote Repository
- Authenticating with Remote Repositories
- Fetching and Pulling Changes
- Pushing Changes
- Tracking Branches
Module 5: Advanced Git Operations
Module 6: Git Tools and Techniques
- Using Git Hooks
- Git Bisect
- Git Blame
- Git Log and Aliases
- Git Submodules
- Multiple Working Copies with git worktree
Module 7: Collaboration and Workflow Strategies
- Forks and Pull Requests
- Code Reviews with Git
- The Git Flow Workflow
- GitHub Flow
- Trunk Based Development
- Continuous Integration with Git
Module 8: Git Best Practices and Tips
- Writing Good Commit Messages
- Keeping a Clean History
- Ignoring Files with .gitignore
- File Attributes with .gitattributes
- Security Best Practices
- Performance Tips
Module 9: Troubleshooting and Debugging
- Common Git Problems
- Undoing Changes
- Resolving Divergence with the Remote
- Recovering Lost Commits
- Dealing with Corrupted Repositories
- Advanced Debugging Techniques
