Push a Git Repository to Two Remotes Safely: Push URLs, Failure Handling, and Verification

Maintained layer, checked 2026-09-01. The short 2019 command note is preserved verbatim at the end. The maintained guide distinguishes alternate push URLs for one logical repository from two genuinely independent hosts, and treats a two-destination push as replication—not as one atomic transaction.

Maintained Guide (2026)

Git can send one push to every pushurl configured for a remote. That makes the original recipe functional, but “simultaneously” is misleading: Git contacts destinations separately, and one can succeed before another fails. For two independent hosting providers, separate named remotes are usually the clearer and safer design.

The examples use a branch named main. Substitute the branch you actually intend to publish, and name it explicitly so repository-local push.default or upstream settings do not change the result.

Choose the Right Model

Situation Recommended model Reason
Two authenticated endpoints that administrators guarantee expose the same logical repository Multiple pushurl values on one remote One remote name can use all configured write endpoints while retaining one fetch URL.
Two independent repositories, providers, credentials, or policy sets Separate remotes such as primary and mirror Each destination can be inspected, pushed, retried, and verified independently.

This distinction comes directly from the official `git remote` documentation: push and fetch URLs for one remote should refer to the same place; if fetching from one place and publishing to another, use separate remotes. The `git push` documentation also confirms that a push affects every defined push URL.

Option A: Multiple Push URLs on One Remote

Assume origin already exists. Set its fetch URL, then add every intended push endpoint—including the primary endpoint:

git remote set-url origin ssh://git@read.example/team/project.git

git remote set-url --add --push origin \
  ssh://git@write-a.example/team/project.git
git remote set-url --add --push origin \
  ssh://git@write-b.example/team/project.git

Once any pushurl exists, Git uses the configured push-URL list instead of falling back to the fetch URL. Adding only the second endpoint would therefore omit the primary endpoint from future pushes.

Inspect fetch and push configuration separately:

git remote get-url --all origin
git remote get-url --all --push origin

To remove one push URL, remember that the final argument is a regular expression. Anchoring it avoids an accidental broad match:

git remote set-url --delete --push origin \
  '^ssh://git@write-b[.]example/team/project[.]git$'

If all custom push URLs are intentionally cleared, Git again falls back to the remote’s fetch URL for pushing:

git config --unset-all remote.origin.pushurl

Do not use git remote rm origin merely to edit a URL: that removes the entire remote and its associated tracking configuration.

Option B: Separate Remotes for Separate Hosts

For independent repositories, give each destination a meaningful name:

git remote add primary ssh://git@code-a.example/team/project.git
git remote add mirror ssh://git@code-b.example/team/project.git

git remote get-url --all --push primary
git remote get-url --all --push mirror

If a named remote already exists, inspect it and use git remote set-url <name> <url> rather than adding a duplicate. Separate names make different credentials, branch rules, error logs, and retries visible instead of hiding them behind origin.

Dry-Run, Push, and Verify

Preview the exact branch against each independent destination:

git push --dry-run --porcelain primary main
git push --dry-run --porcelain mirror main

--dry-run does everything except send the ref updates, while --porcelain provides stable, machine-readable status lines. A dry run is useful but is not a reservation or a proof that every server-side rule will accept the later push.

Push in a deliberate order. With &&, the mirror is skipped if the primary fails:

git push primary main &&
git push mirror main

If the second command fails, the first push is not rolled back. Record the status of each destination and retry only the stale one.

After pushing, compare the local commit ID with the branch advertised by each server:

git rev-parse main
git ls-remote --exit-code --branches primary refs/heads/main
git ls-remote --exit-code --branches mirror refs/heads/main

The first field of both ls-remote lines should equal the value from git rev-parse main. In automation, compare these values exactly and fail the job if either ref is absent or different.

Atomicity and Partial Failure

git push --atomic asks one receiving server to update all refs in that push or none, and fails if that server lacks atomic-push support. It cannot combine two transports or hosts into one transaction. This remains true whether destinations are represented by multiple push URLs or by separate remotes.

A disposable local test with Git 2.43.0 reproduced the practical failure mode:

  • git push --dry-run visited both configured local push URLs and updated neither bare repository.
  • A real push updated both repositories to the same commit.
  • After the first URL was left valid and the second changed to a missing repository, the first updated, the second failed, and git push exited with status 128.

The exact error and exit status can vary by transport, but the partial-success risk is fundamental. A robust replication job must detect and reconcile divergence; adding --atomic does not create cross-host rollback.

Branches, Tags, and Mirror Mode

Be explicit about the ref set:

  • git push <remote> main publishes only main.
  • git push <remote> --all publishes all local branches, not tags.
  • git push <remote> --tags publishes all tags in addition to any explicitly listed refspecs.
  • git push <remote> --mirror mirrors all refs under refs/, force-updates changed refs, and deletes remote refs missing locally.

--mirror is destructive synchronization, not a synonym for “make a backup.” Use it only for a deliberately managed mirror whose complete ref and deletion policy has been reviewed. For normal publication, push named branches and only the tags you intend to distribute.

Credentials and Server-Side Policy

Never embed a password or access token in a remote URL. URLs appear in .git/config, command output, screenshots, and logs. Prefer SSH keys managed by an agent or HTTPS credentials supplied by a secure OS-backed Git credential helper. Authenticate each independent host and grant only the repository permissions that push requires.

The archived commands use git://. Git’s `git daemon` documentation says the protocol has no authentication and enables only the read-oriented upload-pack service by default; anonymous receive-pack is disabled by default. Do not reuse those historical URLs as an authenticated-write template.

Each server evaluates its own protected-branch rules, permissions, non-fast-forward policy, hooks, required signatures, and other hosting controls. Generic Git servers can reject non-fast-forward pushes with `receive.denyNonFastForwards`, while `pre-receive` and `update` hooks can reject proposed ref updates. A client-side force option does not override those server decisions. Align policy on both hosts or expect one destination to reject changes that the other accepts.

Practical Automation Checklist

  1. Use separate remotes for independent hosts and an explicit branch or refspec.
  2. Keep secrets out of URLs and logs; test authentication to each destination independently.
  3. Run per-remote dry runs as a preview, not as a guarantee.
  4. Push in an intentional order and retain the result for each destination.
  5. Compare the advertised remote branch IDs with the intended local commit.
  6. Retry only failed or stale destinations and alert when divergence remains.

This turns “push twice” into an observable replication process without pretending it has distributed-transaction guarantees.

Primary Documentation

Original 2019 Archive (Verbatim)

The following is the complete visible body from the original WordPress export, published and last modified on 2019-04-24. Only invisible trailing whitespace has been normalized for repository formatting. Its unauthenticated git:// examples and lack of failure handling are preserved as historical provenance, not as the maintained recommendation.

git remote set-url –add –push origin git://original/repo.git
git remote set-url –add –push origin git://another/repo.git


Remove one url from origin

git remote set-url –delete –push origin git://another/repo.git


Remove origin totally

git remote rm origin

git remote set-url origin git://another/repo.git

Leave a Reply