How to Save a GitHub Repository Locally: A Backup Guide That Actually Restores
"Save a GitHub repository locally" means three different things to three different people: a folder of current files, a copy of the full history, or a snapshot you could rebuild the repository from. Only the last one is a backup. This guide covers all three, in order of how much they preserve, and shows how to verify each.
What a repository actually contains
Before choosing a method, it helps to know what you would be leaving behind. A GitHub repository is more than files:
- Commits and their history, across every branch
- Tags, which is how releases are named
- Remote-tracking branches — work that exists on the server but not in your working copy
- Ancillary data: issues, pull requests, comments, release notes, wiki, labels
Git tooling can take the first three. The fourth lives in GitHub's API and needs separate handling — see the last section.
Option 1: Download a zip (files only)
Fastest, weakest. On the repository page: Code → Download ZIP. You get the files of one branch at one commit, with no history and no branches.
curl -L -o repo.zip https://github.com/OWNER/REPO/archive/refs/heads/main.zip
unzip -q repo.zip -d repo-files
Good for: grabbing code to read, or archiving a snapshot of a finished project.
Not good for: backup. You cannot restore a repository from it, cannot see why a line changed, and cannot recover a deleted branch.
Option 2: A mirror clone (the real backup)
A mirror clone copies every ref the server has — all branches, all tags — into a bare repository with the remote configuration intact. This is the right tool for backup.
git clone --mirror https://github.com/OWNER/REPO.git repo.git
A bare repository (repo.git) has no working tree; it is just the database. That is correct for a backup: complete, compact, and directly pushable back to a new host.
To update an existing mirror later, do not re-clone — fetch:
cd repo.git
git fetch --prune origin
--prune is important. It removes refs deleted upstream, so the mirror stays a true reflection rather than accumulating branches nobody remembers.
Make it space-efficient
Mirrors include everything, including release assets if you fetch them separately (git does not store those). To keep size reasonable:
git clone --mirror https://github.com/OWNER/REPO.git repo.git
cd repo.git
git gc --aggressive --prune=now
du -sh .
Many repositories at once
With gh installed, loop over every repository in an organisation:
gh repo list OWNER --limit 400 --json nameWithOwner,isFork \
--jq '.[] | select(.isFork==false) | .nameWithOwner' \
| while read -r repo; do
dest="mirrors/${repo//\//__}.git"
if [ -d "$dest" ]; then
git -C "$dest" fetch --prune origin
else
git clone --mirror "https://github.com/$repo.git" "$dest"
fi
done
Run it on a schedule — nightly for active repositories, weekly for everything else — and you have a copy that does not depend on one account surviving.
Option 3: A git bundle (one file per repository)
A bundle packs reference and object history into a single file. It is ideal for cold storage, since one file is easy to copy, checksum and ship to offsite storage.
git clone --mirror https://github.com/OWNER/REPO.git repo.git
cd repo.git
git bundle create ../repo-backup.bundle --all
git bundle verify ../repo-backup.bundle
--all includes branches and tags. verify confirms the file is intact and that you have the prerequisites to read it — an easy habit that catches truncated copies before they matter.
Restoring from a bundle is a clone:
git clone repo-backup.bundle restored-repo
cd restored-repo
git remote set-url origin https://github.com/OWNER/NEW-REPO.git
git push --mirror origin
Verifying a backup (skip this and you have a rumour, not a backup)
A backup you have never restored is a hope. Two checks are enough:
- Does the copy have the refs?
Compare the branch and tag counts with the GitHub repository's branch and tag lists. They should match.git -C repo.git for-each-ref --format='%(refname)' | sort | head -50 git -C repo.git branch -a | wc -l git -C repo.git tag | wc -l - Does the newest commit survive?
The hash should match the latest commit on the server.git -C repo.git log -1 --format='%H %ci %s' origin/main
Once a quarter, do a real restore into a scratch directory and confirm the code builds or tests run. Restoring into a fresh folder is the only test that catches every class of problem — missing refs, missing large files, wrong remote.
What git does not save
Issues, pull requests, comments, releases and wikis are not in the git database. If those matter, capture them too:
gh issue list --repo OWNER/REPO --state all --limit 1000 \
--json number,title,body,state,createdAt,closedAt,labels \
> issues.json
gh pr list --repo OWNER/REPO --state all --limit 1000 \
--json number,title,body,state,mergedAt,author \
> pull-requests.json
For wikis, clone them like a repository: git clone https://github.com/OWNER/REPO.wiki.git. For release assets — binaries attached to releases — download them with gh release download per tag.
Where to store the copy
Three rules, in order of how often they are broken:
- Not on the same laptop as the only working copy. A backup on the machine you develop on dies with the machine.
- Not in the same account. A mirror stored in the same GitHub organisation is not protection against losing the organisation.
- Somewhere you will notice it failing. Scheduled backups that silently stop are worse than none, because you budget for them.
A practical arrangement for a small team: nightly mirrors to a team server or object-storage bucket, weekly bundles to cold storage, and a quarterly restore drill with a calendar reminder. Encrypt at rest if the code is not public, and keep the restore instructions next to the copy — future-you will not remember the commands.
A five-minute version
If you only do one thing today:
git clone --mirror https://github.com/OWNER/REPO.git REPO.git
cd REPO.git && git bundle create ../REPO-$(date +%F).bundle --all
git bundle verify ../REPO-$(date +%F).bundle
You now have a full-history copy in one portable file. Do that for every repository that would hurt to lose, and put the copies somewhere other than your laptop.
Keep the list, not just the copies
The most common failure in repository backup is not a corrupt file — it is a repository nobody remembered to include. Keep an inventory with one row per repository: name, owner, what it is, whether it is backed up, when it was last verified. That list is also the thing that tells you which of your two hundred repositories you can stop backing up altogether.
repos.to builds that inventory and sets up the backup routine for small teams — mirror clones on a schedule, a verified bundle in cold storage, and a restore drill you can hand to anyone on the team.