How to Mirror a Public GitHub Repository to Your Own Private Repository
If you depend on a public GitHub repository and want a copy that does not disappear just because the upstream project is deleted, renamed, or made unavailable, one practical option is to keep a full Git mirror in a private repository under your own account.
This is different from a normal git clone. The combination of git clone --mirror and git push --mirror is designed to synchronize Git refs, making it useful for source-level backups. It is not a full backup of everything GitHub stores: Issues, Pull Requests, Release assets, repository settings, Secrets, and other platform data are not included. Repositories that use Git LFS also require a separate LFS object migration.
Warning:
git push --mirrormakes the destination refs match the local mirror. It may force-update refs or delete destination refs that no longer exist in the mirror. Use a dedicated backup repository rather than a normal development repository.
When a mirror backup makes sense
This approach is useful when you:
- Depend on an open-source repository and want to preserve its complete Git history.
- Want protection against an upstream repository being deleted, renamed, or made inaccessible.
- Need to periodically synchronize upstream branches and tags without using a Fork for collaboration.
- Want the backup to remain Private and serve only as a recovery copy.
If your goal is to modify the project and contribute changes upstream, a Fork is usually a better fit. If you only need a temporary local copy, a normal clone is enough.
What a Git mirror does and does not copy
git clone --mirror creates a bare mirror repository configured to track remote refs. With git push --mirror, those refs can be synchronized to another Git repository.
It covers Git-level data such as:
- Commit history
- Branch refs
- Tag refs
- Other visible remote Git refs
- Git objects
It does not automatically copy GitHub platform data such as:
- Issues
- Pull Requests
- Discussions
- Release assets
- Actions Secrets and Variables
- Webhooks
- Collaborators and permissions
- Branch protection and rulesets
- Repository visibility settings
Git LFS is also separate: Git history contains LFS pointers, while the large-file objects themselves must be transferred independently.
Prerequisites
Install these tools locally:
- Git
- GitHub CLI (
gh) - Git LFS if the upstream repository uses LFS
Verify your GitHub CLI login:
gh auth status
gh auth setup-git
If you are not logged in yet:
gh auth login
Do not put PATs, passwords, or other credentials directly into repository URLs, shell history, or documentation. Prefer GitHub CLI credential setup or SSH.
Create the private mirror for the first time
Suppose the public repository is:
https://github.com/OWNER/REPOSITORY
Run the following from the directory where you want to store the local mirror:
set -euo pipefail
UPSTREAM_URL="https://github.com/OWNER/REPOSITORY.git"
REPO_NAME="REPOSITORY"
ACCOUNT="$(gh api user --jq .login)"
BACKUP_REPO="$ACCOUNT/$REPO_NAME"
BACKUP_URL="https://github.com/$BACKUP_REPO.git"
MIRROR_DIR="$REPO_NAME.git"
if gh repo view "$BACKUP_REPO" >/dev/null 2>&1; then
echo "Target repository already exists: $BACKUP_REPO"
exit 1
fi
if [ -e "$MIRROR_DIR" ]; then
echo "Local path already exists: $MIRROR_DIR"
exit 1
fi
gh repo create "$BACKUP_REPO" --private
git clone --mirror "$UPSTREAM_URL" "$MIRROR_DIR"
git -C "$MIRROR_DIR" remote add backup "$BACKUP_URL"
git -C "$MIRROR_DIR" push --mirror backup
Two safeguards in this script are intentional:
- Stop if the destination repository already exists. Do not overwrite it by default.
- Stop if the local mirror directory already exists. Verify that it belongs to the same upstream before reusing it.
The destination repository’s Private visibility is controlled by GitHub repository settings. A Git push does not change visibility.
Why not use a normal clone and push?
A normal working clone is optimized for development: checked-out files, current branches, and remote-tracking branches.
A backup mirror has a different goal. You want a bare repository that tracks upstream refs and can reproduce those refs in another repository.
git clone --mirror "$UPSTREAM_URL" "$MIRROR_DIR"
The resulting directory has no working tree. For a dedicated synchronization or backup task, that is often exactly what you want.
Incremental updates
After the first mirror, there is no need to clone again.
From the parent directory of REPOSITORY.git, run:
git -C REPOSITORY.git fetch --prune origin
git -C REPOSITORY.git push --mirror backup
Here:
originis the public upstream.backupis your private destination.
fetch --prune origin updates the local mirror and removes remote refs that disappeared upstream. Then push --mirror backup aligns the destination with the current mirror state.
If you keep mirrors under a fixed directory:
git -C "$HOME/git-mirrors/REPOSITORY.git" fetch --prune origin
git -C "$HOME/git-mirrors/REPOSITORY.git" push --mirror backup
Git LFS needs a separate transfer
If the project uses Git LFS, synchronizing Git refs is not enough.
Check that Git LFS is installed:
git lfs version
Then synchronize LFS objects from the upstream and push them to the backup:
git -C REPOSITORY.git lfs fetch --all origin
git -C REPOSITORY.git lfs push --all backup
For recurring updates:
git -C REPOSITORY.git fetch --prune origin
git -C REPOSITORY.git lfs fetch --all origin
git -C REPOSITORY.git lfs push --all backup
git -C REPOSITORY.git push --mirror backup
Running --all on a large repository can take time and consume LFS storage and bandwidth on the destination account.
Verify that the mirror is really correct
First confirm that the destination is Private:
ACCOUNT="$(gh api user --jq .login)"
gh repo view "$ACCOUNT/REPOSITORY" --json nameWithOwner,visibility,defaultBranchRef
Then compare the refs and object IDs exposed by both repositories:
UPSTREAM_URL="https://github.com/OWNER/REPOSITORY.git"
ACCOUNT="$(gh api user --jq .login)"
BACKUP_URL="https://github.com/$ACCOUNT/REPOSITORY.git"
git ls-remote --refs "$UPSTREAM_URL" | awk '{print $1, $2}' | sort > /tmp/upstream-refs.txt
git ls-remote --refs "$BACKUP_URL" | awk '{print $1, $2}' | sort > /tmp/backup-refs.txt
diff -u /tmp/upstream-refs.txt /tmp/backup-refs.txt
No diff output with exit code 0 means the listed refs and object IDs match.
For an LFS repository, also clone or check out the private mirror once and confirm that the large files can actually be downloaded.
The biggest push --mirror risk
The most important rule is:
A mirror is synchronization, not append-only backup.
If the destination has refs that do not exist upstream, git push --mirror may delete them. If the same ref points to a different object, it may be force-updated.
A safe operating model is therefore:
- One upstream repository per dedicated private mirror.
- Do not develop extra branches in the mirror repository.
- Do not use the mirror as a normal collaboration repository.
- Verify that the destination is empty before the first sync.
- Run the full process manually once before automating it.
Automating mirror updates
For long-term backups, the recurring core can be reduced to:
git -C "$MIRROR_DIR" fetch --prune origin
if git -C "$MIRROR_DIR" lfs env >/dev/null 2>&1; then
git -C "$MIRROR_DIR" lfs fetch --all origin
git -C "$MIRROR_DIR" lfs push --all backup
fi
git -C "$MIRROR_DIR" push --mirror backup
Before putting this into cron or CI, add guardrails:
- Pin the exact allowed upstream URL.
- Pin the exact allowed destination repository.
- Exit immediately if the destination is not the expected repository.
When push --mirror becomes unattended, preventing a push to the wrong target matters more than saving a few lines of shell.
Using a CLI AI agent safely
If you ask a CLI coding agent to perform the backup, make the safety boundaries explicit:
Mirror the public GitHub repository I provide into a same-named Private repository under my currently authenticated GitHub account.
Requirements:
1. Check git, gh, and gh auth status first.
2. Parse the repository name from the public URL.
3. Before creating anything, check whether the same-named destination repository already exists. If it does, stop and report; do not overwrite it.
4. Check whether the local mirror directory already exists. If its purpose is unclear, stop and do not delete it.
5. Create an empty Private repository, not a Fork.
6. Clone the upstream with git clone --mirror.
7. Add a backup remote and run git push --mirror backup.
8. If the repository uses Git LFS, synchronize all required LFS objects.
9. Verify that the destination visibility is PRIVATE and compare git ls-remote --refs on both sides.
10. Print the recurring fetch --prune + push --mirror commands.
Safety boundaries:
- Never print or request tokens, passwords, cookies, or private keys.
- Never modify the public upstream repository.
- Never delete existing repositories or unknown local directories.
- Stop if the destination already contains content.
- Do not run production deployment or unrelated operations.
The useful part is not just telling the agent to “back up the repository.” It is defining the conditions under which it must stop.
What this solution does not replace
This is a Git repository mirror, not a complete GitHub disaster-recovery system.
A full project archive may also need:
- Issues and Pull Requests
- Releases and attached files
- Wiki content
- Actions configuration and variables
- Repository permissions and rules
- Packages and container images
- Discussions
But if your primary goal is simple — “even if the public repository disappears, I still have the full source history, branches, and tags” — clone --mirror + push --mirror is a transparent and verifiable solution.
Related reading
- Back Up Notion to GitHub with the Official API and GitHub Actions
- Automatically Back Up Docker Volumes to GitHub