# How to Mirror a Public GitHub Repository to Your Own Private Repository

> Canonical: https://vxi.me/en/posts/github-public-repo-private-mirror-backup/
> Locale: en-us
> Published: 2026-10-05
> Updated: 2026-10-05
> Markdown: https://vxi.me/en/posts/github-public-repo-private-mirror-backup.md

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 --mirror` makes 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:

```bash
gh auth status
gh auth setup-git
```

If you are not logged in yet:

```bash
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:

```text
https://github.com/OWNER/REPOSITORY
```

Run the following from the directory where you want to store the local mirror:

```bash
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:

1. **Stop if the destination repository already exists.** Do not overwrite it by default.
2. **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.

```bash
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:

```bash
git -C REPOSITORY.git fetch --prune origin
git -C REPOSITORY.git push --mirror backup
```

Here:

- `origin` is the public upstream.
- `backup` is 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:

```bash
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:

```bash
git lfs version
```

Then synchronize LFS objects from the upstream and push them to the backup:

```bash
git -C REPOSITORY.git lfs fetch --all origin
git -C REPOSITORY.git lfs push --all backup
```

For recurring updates:

```bash
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:

```bash
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:

```bash
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:

```bash
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:

```text
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](/en/posts/notion-api-github-actions-backup/)
- [Automatically Back Up Docker Volumes to GitHub](/en/posts/docker-volume-backup-github-sync/)

## Official references

- [GitHub Docs: Duplicating a repository](https://docs.github.com/en/repositories/creating-and-managing-repositories/duplicating-a-repository)
- [GitHub CLI: gh repo create](https://cli.github.com/manual/gh_repo_create)
- [Git: git push](https://git-scm.com/docs/git-push)
- [Git LFS: git lfs fetch](https://github.com/git-lfs/git-lfs/blob/main/docs/man/git-lfs-fetch.adoc)
- [Git LFS: git lfs push](https://github.com/git-lfs/git-lfs/blob/main/docs/man/git-lfs-push.adoc)
