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

  1. When to suspect corruption and when it is something else
  2. The real causes of corruption
  3. git fsck as a diagnostic tool
  4. Repairs, from least to most serious
  5. The safety net of the distributed model
  6. Rebuilding a repository while keeping your work
  7. Prevention: maintenance and proper backups
  8. When to stop repairing and clone again

  1. 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:

# 1. Is there space on the disk? It is the number one cause of half-finished writes
df -h .
Filesystem      Size  Used Avail Use% Mounted on
/dev/sda2       234G  234G     0 100% /home

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)" | head
# 3. Where does the repository live?
df -T . | tail -1
/dev/sda2 ext4 ...

If 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.

  1. 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 damage

If 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:

Exclude from real-time protection:
  C:\Users\carla\projects\           (or at least the .git folders)

And in Git for Windows, a setting that reduces problems with antivirus software and with slow filesystems:

git config --global core.fscache true

  1. git fsck as a diagnostic tool

git 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.

git fsck --full --no-progress

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

git fsck --full --no-progress
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 8a1f6c3d5e2b9c7f4a6d1e8b3c5f7a9d2e4b6c8a

Here 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

git cat-file -t 4f8a2e6      # type
git cat-file -s 4f8a2e6      # size
git cat-file -p 4f8a2e6      # content
fatal: Not a valid object name 4f8a2e6

That message, for a hash you know existed, is confirmation that the object has disappeared.

  1. Repairs, from least to most serious

4.1. A corrupt .git/index (the most frequent and mildest case)

error: bad index file sha1 signature
fatal: index file corrupt

or

fatal: index file smaller than expected

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 HEAD and 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 status
git status --short
 M app.js
 M styles.css
?? draft.txt

The 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:

git merge --abort 2>/dev/null || true
rm -f .git/index
git reset
git merge <branch>        # start again

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:

find .git -name "*.lock"
.git/index.lock
.git/refs/heads/GT-241.lock
.git/config.lock

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 status

A 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:

git config --list --local
cat .git/config

If it is broken, the local configuration is the easiest thing in the whole repository to redo by hand.

4.3. Broken references

error: refs/heads/GT-241 does not point to a valid object!

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):

cat .git/refs/heads/GT-241
git cat-file -t "$(cat .git/refs/heads/GT-241)"
b52c9d1e4a7f3c8b6d2e9a5f1c7b3d8e4a6f2c9b
fatal: Not a valid object name b52c9d1e4a7f3c8b6d2e9a5f1c7b3d8e4a6f2c9b

Confirmed: the reference points at nothing.

Repair A: the branch's reflog. It is the best option, because it preserves history:

git reflog show GT-241
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 main

Try 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 -3

You 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):

grep GT-241 .git/packed-refs
7d3a8f4a2c6e9b1d5f3a8c6e2b9d4f7a1c5e8b3d refs/heads/GT-241

Sometimes the packed version is valid even though the loose one is broken. If you delete the loose one, Git uses the packed one:

rm .git/refs/heads/GT-241
git log --oneline GT-241 -3

Repair C: the remote.

git fetch origin
git update-ref refs/heads/GT-241 origin/GT-241

Repair D: if the branch does not matter, delete it.

git update-ref -d refs/heads/GT-241

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"
done

4.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:

ls -l .git/objects/4f/8a2e6c9b1d5e3a7f2c8b6d9e4a1f5c3b7d2e9a
-r--r--r-- 1 ana ana 0 Jul 28 16:04 .git/objects/4f/8a2e6c9b...

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 4f8a2e6c9b1d5e3a7f2c8b6d9e4a1f5c3b7d2e9a

The 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-object
4f8a2e6c9b1d5e3a7f2c8b6d9e4a1f5c3b7d2e9a

If 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:

fatal: packed object 4f8a2e6... (stored in .git/objects/pack/pack-a1b2c3.pack) is corrupt

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 | head

But 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

fatal: bad object HEAD
fatal: not a git repository (or any of the parent directories)

HEAD is a one-line text file (lesson 03-01):

cat .git/HEAD

What it should contain:

ref: refs/heads/main

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 -3

If no valid branch remains, the HEAD reflog is still plain, readable text even when Git does not work:

tail -5 .git/logs/HEAD
... 7d3a8f4a2c6e9b1d ... commit: GT-241 calculate the counter
git update-ref refs/heads/rescue 7d3a8f4
git symbolic-ref HEAD refs/heads/rescue

4.6. A corrupt .git/config

fatal: bad config line 12 in file .git/config

It is the easiest file to repair, because its content is trivial to redo:

cat .git/config
[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/main

If 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 "ana.ferrer@example.com"

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

  1. 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/heads
main       upstream=[origin/main]
GT-241     upstream=[]
GT-238     upstream=[origin/GT-238]

GT-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 --short

With 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.

  1. 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

cd ~/projects
cp -a task-manager task-manager-BROKEN-$(date +%Y%m%d-%H%M)

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/null

Step 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.bundle
The 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-manager

Step 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 branch
  GT-238
  GT-241
* main

The 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 status

If there are no errors and all your work is there:

rm -rf ~/projects/task-manager-broken
rm /tmp/rescue.bundle /tmp/*.patch

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

  1. Prevention: maintenance and proper backups

Periodic git maintenance and git fsck

Picking up from lesson 08-06:

# Scheduled maintenance in the background
git maintenance start

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 true

It 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-restored

And incremental copies, if the repository is large:

# Only what comes after a tag
git bundle create ~/backups/since-v1.5.bundle v1.5.0..main

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 -delete

Why 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.

  1. 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 fsck returns 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 clone is minutes; that of an afternoon of archaeology is not.

It is worth attempting the repair when:

  • The problem is the index, a lock, HEAD or config. It is minutes and nothing is lost.
  • It is one broken reference and the reflog or packed-refs resolves 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

  1. Create a repository with ten commits, a branch with three commits of its own and an uncommitted git add.
  2. Produce dangling objects: a reset --hard HEAD~3, a commit --amend and a branch -D.
  3. Run git fsck --full and count how many lines it returns.
  4. Apply the filter from tip 1 and check that nothing is left. Explain why the repository is healthy despite the earlier lines.
  5. Identify, among the dangling ones, which corresponds to the uncommitted git add and recover it with git cat-file -p.

Exercise 2: breaking and repairing

On a test repository (never on a real one):

  1. Corrupt the index: printf 'rubbish' > .git/index. Run git status and note the error. Repair it and explain why nothing is lost.
  2. Break a reference: echo "0000000000000000000000000000000000000000" > .git/refs/heads/GT-241. Run git fsck and repair it with the reflog.
  3. Break HEAD: echo "ref: refs/heads/nonexistent" > .git/HEAD. Diagnose and repair with git symbolic-ref.
  4. Empty a loose object: locate one with find .git/objects -type f and truncate it with : > <file>. Run git fsck --full and observe the error.
  5. Repair point 4 from a healthy clone that you will have made before breaking anything.
  6. After each repair, run a filtered git fsck --full and confirm that it is clean.

Exercise 3: a complete rescue with bundle

  1. Set up a local remote with git init --bare and clone it.
  2. In the clone, publish two commits on main and create two unpublished local branches with three commits each. Add a stash and some uncommitted change.
  3. Do the inventory from section 5: which branches have no upstream, which commits are not on the remote.
  4. Create a bundle with all the local material and verify it.
  5. Delete the whole clone (simulating irreparable corruption) and clone again.
  6. Bring the two branches back from the bundle and check that the six commits are there.
  7. 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 "ana.ferrer@example.com"
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
Deleted branch GT-241 (was 8f4c2a9).
# 3. The complete output
git fsck --full --no-progress 2>&1 | wc -l
git fsck --full --no-progress 2>&1 | head
9
Checking 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.

# 4. The filter
git fsck --full --no-progress 2>&1 | grep -vE "^(dangling|notice|Checking)"
(no output)

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
done
--- 6f2b9d4a8c1e5f3b7d9a2c6e4f8b1d3a5c7e9f2b ---
STAGED BUT NOT COMMITTED
git cat-file -p 6f2b9d4 > styles.css && cat styles.css
STAGED BUT NOT COMMITTED

Solution 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 "bruno.salas@example.com"
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-healthy
# 1. Corrupt index
echo "UNCOMMITTED WORK" >> app.js
printf 'rubbish' > .git/index
git status
error: bad index file sha1 signature
fatal: index file corrupt
rm -f .git/index
git reset
git status --short
tail -1 app.js
 M app.js
UNCOMMITTED WORK

Nothing 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)"
error: refs/heads/GT-241 does not point to a valid object!
git log --oneline GT-241 -1
fatal: bad object GT-241
git reflog show GT-241
b52c9d1 GT-241@{0}: commit: GT-241 filter
7d3a8f4 GT-241@{1}: branch: Created from HEAD
git cat-file -t b52c9d1        # check that the object exists
git update-ref refs/heads/GT-241 b52c9d1
git log --oneline GT-241 -2
commit
b52c9d1 (GT-241) GT-241 filter
7d3a8f4 (HEAD -> main) c5

Repaired, complete with its commit. The reflog kept the correct hash because the reflog file is independent of the reference file.

# 3. Broken HEAD
echo "ref: refs/heads/nonexistent" > .git/HEAD
git status
On branch nonexistent

No commits yet

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
GT-241  main
## main
7d3a8f4 c5
# 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)"
-rw-r--r-- 1 bruno bruno 0 Jul 31 12:44 .git/objects/6f/2b9d4a8c1e5f3b...
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 log --oneline -1
fatal: loose object 6f2b9d4... 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"
Missing: 6f2b9d4a8c1e5f3b7d9a2c6e4f8b1d3a5c7e9f2b
blob
# 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"
Written: 6f2b9d4a8c1e5f3b7d9a2c6e4f8b1d3a5c7e9f2b
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 -3
(no output)
2f8c6e1 (HEAD -> main) c6
7d3a8f4 c5
b52c9d1 (GT-241) GT-241 filter

Solution 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 "carla.vidal@example.com"

# 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.txt

Six 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.bundle
The 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 --all
b52c9d1 (HEAD -> main, origin/main) feat: task list
1e6f2c8 chore: start

Only 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 -3
  GT-241
  GT-247
* main
9c4e7b2 (GT-241) GT-241 part 3
3e7f1a8 GT-241 part 2
8a1f6c3 GT-241 part 1
2f8c6e1 (GT-247) GT-247 part 3
7a4d9b3 GT-247 part 2
5c1e8f2 GT-247 part 1

The 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 --short
 M app.js
?? draft.txt
git fsck --full --no-progress 2>&1 | grep -vE "^(dangling|notice|Checking)"
(no output)
# 7. What has been lost
git stash list
git reflog | wc -l
(no output)
3

What 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 --all would have included refs/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.

git push -q -u origin GT-241 GT-247

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 -h and ls -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 fsck is read by filtering out the noise. Dangling objects are completely normal — they are the remains of every reset, rebase and --amend — and their presence is proof that the safety net from lesson 09-04 works. The serious stuff starts with error:, missing or broken link.
  • The repairs, from least to most serious: the index is a derived cache and is rebuilt with rm .git/index && git reset without losing anything; locks are deleted after checking processes; a broken reference is repaired with the reflog, packed-refs or the remote; HEAD with git 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-ref and git log --branches --not --remotes.
  • The rescue is done with git bundle: pack the local material into a file, re-clone, and bring it back with git fetch <bundle>. And always with a prior cp -a and a subsequent verification before deleting anything.
  • Prevention: no repositories in the cloud or on network volumes, git maintenance start, transfer.fsckObjects true, a periodic git bundle and — 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, HEAD and 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

Module 2: Basic Git Operations

Module 3: Branching and Merging

Module 4: Working with Remote Repositories

Module 5: Advanced Git Operations

Module 6: Git Tools and Techniques

Module 7: Collaboration and Workflow Strategies

Module 8: Git Best Practices and Tips

Module 9: Troubleshooting and Debugging

Module 10: Git in the Real World

© Copyright 2026. All rights reserved