●OPUS48 — Claude Opus 4.8 is generally available, improving on 4.7 across coding, agentic skills, reasoning, and knowledge work●AUTO — Claude Code now defaults to auto mode on Bedrock, Vertex AI, and Foundry, and updates Bedrock to Opus 4.8●GUARD — Auto mode now blocks tampering with session transcripts and asks before running rm -rf on an unresolved variable●IDP — Admins can provision MCP connectors org-wide via their IdP (starting with Okta), granting access on first login●TIMEOUT — Fixed per-server request_timeout_ms being ignored, which timed out long MCP tool calls at the 60s default●FAST — Fast mode for Opus 4.7 is deprecated and will be removed on July 24; migrate to fast mode for Opus 4.8●OPUS48 — Claude Opus 4.8 is generally available, improving on 4.7 across coding, agentic skills, reasoning, and knowledge work●AUTO — Claude Code now defaults to auto mode on Bedrock, Vertex AI, and Foundry, and updates Bedrock to Opus 4.8●GUARD — Auto mode now blocks tampering with session transcripts and asks before running rm -rf on an unresolved variable●IDP — Admins can provision MCP connectors org-wide via their IdP (starting with Okta), granting access on first login●TIMEOUT — Fixed per-server request_timeout_ms being ignored, which timed out long MCP tool calls at the 60s default●FAST — Fast mode for Opus 4.7 is deprecated and will be removed on July 24; migrate to fast mode for Opus 4.8
One Day My Push Had an Extra Destination — Guarding Against /commit-push-pr Pushing to Remotes Beyond origin
The July 14 update made /commit-push-pr push to configured push remotes in addition to origin. Convenient, but if you keep a mirror or backup as a second remote, unintended pushes quietly multiply. Here is how to inventory which remotes you can push to, block anything off the allowlist with a pre-push hook, and keep unattended runs safe — with working code.
After a nightly run finished, I was scanning the push log out of habit when I noticed one line too many. The push to origin looked normal. Below it sat another remote name — familiar, but not one I expected to see there. I had registered that remote only to read from it. I never meant to push to it, yet there it was. The numbers and the diff were correct; the delivery just had one more destination than I intended. That small surplus was the behavior change the July 14 update introduced.
Until then, /commit-push-pr assumed a push to origin. After the update, it also auto-approves pushing to configured push remotes. If you run on a single remote, this change never surfaces. But as a personal developer juggling several sites, with even one mirror or backup registered as a second remote, your push quietly gains a destination. Today is my record of inventorying that change against my own nightly setup and narrowing it down to only the remotes I actually want to push to.
The opposite failure — a push that reports success but delivers nothing — I covered in when a push reports success but nothing lands. This piece guards the other side: something arriving where you never meant to send it.
Watching Only origin Hides the New Destination
We got away with ignoring push targets because, in most repositories, the target was a single remote called origin. Commit, push, one round trip — with only one remote in the loop, there is nothing to think about regarding where it lands.
This change quietly shifts that premise. When /commit-push-pr targets "origin plus configured push remotes," the moment you add a remote, your push destination can grow. And it happens not because you explicitly said "push here too," but as the command's default behavior.
The awkward part is that remotes are usually registered for reasons unrelated to pushing: to pull in someone's fork, to reference a mirror of another environment, or simply to inspect a diff. Each of those is a remote added to read — now silently eligible to be written to.
Which Remotes Can You Actually Push To Right Now
Before any fix, you need an accurate picture. The baseline is one line:
git remote -v
What people miss here is that fetch and push URLs are listed separately. The same remote name can point to different places for reading and for sending.
A remote like mirror, whose push URL points somewhere else entirely, is exactly the destination you do not want to hit by accident. Beyond the remotes themselves, some settings decide the push target at a higher level. Three configs are worth pinning down.
Setting
Role
Check command
remote.<name>.pushurl
Sends that remote's pushes to a different URL
git config --get-all remote.mirror.pushurl
remote.pushDefault
Changes the default push remote away from origin
git config --get remote.pushDefault
branch.<name>.pushRemote
Overrides the push target per branch
git config --get branch.main.pushRemote
With none of these set, a push heads straight to origin. With even one of them set, you cannot infer the target from the command alone. I keep a one-liner that dumps all three as a standard repository check.
What follows includes implementation code, benchmarks, and practical content we hope you'll find useful. This site runs without ads — server and development costs are supported entirely by members like you. If it's been helpful, we'd be truly grateful for your support.
WHAT YOU'LL LEARN
✦Understand exactly what changed — /commit-push-pr now pushes to configured push remotes, not just origin — and read your git config to see every remote you can currently push to
✦Map the typical ways push targets grow through pushurl, remote.pushDefault, and per-branch pushRemote, and block anything off your allowlist with a pre-push hook
✦With a verification script you can confirm the hook actually fires, ready to drop into an unattended nightly run where no confirmation prompt is available
Secure payment via Stripe · Cancel anytime
✦
Unlock This Article
Get full access to the rest of this article. Buy once, read anytime. This site is ad-free — your support goes directly toward keeping it running.
The paths by which a push gains a destination are fairly predictable. In rough order of how easily a personal developer trips over them:
A backup mirror registration — you added mirror to keep a copy on another service, but you also set a pushurl, making it writable.
Pulling in an upstream fork — you registered upstream meaning it to be fetch-only, yet its push URL is not empty and gets picked up as a target.
A leftover test remote — you pushed to another environment once, forgot to git remote remove, and the registration lingers.
Every one of these is a remote added "to read" or "left behind," with no malice and no awareness of a mistake. That is precisely why, when the command widens its scope, the push lands quietly. The goal is not to delete every remote. Keep reference remotes as references, and restrict pushes to an allowed set. Draw that line, and you keep the convenience while removing the accident.
Three Layers to Stop an Unintended Push
One layer is not enough. I think in three.
The first layer is the inventory. Use the commands above to surface every remote you can push to, and strip any pushurl you did not intend. For read-only remotes, you can also assign an invalid push URL to seal off writes.
The second layer is the hook. An inventory goes stale after a single pass, so you insert something that mechanically checks against an allowlist on every push. That is the heart of this piece.
The third layer is a pre-run check. Unattended runs cannot use interactive confirmation, so log which remotes the config allows pushing to at the very start of the job. Later, reading the log, you can verify the destination was what you expected.
Block Off-Allowlist Remotes With a pre-push Hook
The pre-push hook fires just before a push actually goes to the network, and if it exits non-zero, the push itself aborts. Put an allowlist check here and, even when the command widens the destination, you hold the last line.
First, the bare state before the hook. Any remote passes straight through.
# Before: no hook. Every remote /commit-push-pr targets gets pushed as-is$ git push mirror main# → reaches backup.example with no confirmation
With the hook in place, remote names not on the allowlist are rejected. Drop this in .git/hooks/pre-push and make it executable. Git passes the target remote name as the first argument.
#!/usr/bin/env bash# .git/hooks/pre-push# Only let pushes through to allowed remotes; abort otherwise.set -euo pipefailALLOWED="origin" # space-separated for multiple: "origin release"remote_name="$1" # the push target remote nameis_allowed=0for r in $ALLOWED; do if [ "$remote_name" = "$r" ]; then is_allowed=1 break fidoneif [ "$is_allowed" -ne 1 ]; then echo "pre-push: blocked push to '$remote_name' (allowed: $ALLOWED)" >&2 echo "pre-push: add it to ALLOWED only if you truly intend to push there" >&2 exit 1fiexit 0
$ chmod +x .git/hooks/pre-push# After: a push to a non-allowed remote is aborted by the hook$ git push mirror mainpre-push: blocked push to 'mirror' (allowed: origin)error: failed to push some refs to '...'
The strength of this approach is that whatever remotes /commit-push-pr targets internally, you hold the final exit in one place. If the command's behavior shifts again later, the judgment — the allowlist — stays on your side. There is one pitfall. .git/hooks is not included in a clone, so the hook is lost every time you re-clone the repository. In a setup that pushes from disposable clones at night, that gap turned into a real incident in production for me. As a countermeasure, I recommend folding the hook placement into your pre-push routine right after cloning. Pointing core.hooksPath at a shared directory and keeping the hook body under version control works well too.
Notes for Unattended and Scheduled Runs
In an interactive session, an extra destination leaves room to notice — "wait, that's one target too many." An unattended run has no such room. The confirmation prompt either does not appear or has no one to answer it. That is exactly why the correctness of the target has to be guaranteed structurally, before the run.
What I keep as a habit is stamping the "remotes I could push to" into the log at the very start of the job.
Those few lines let you confirm at a glance, when you read the log later, whether the destination really was only origin. The hook is the first barrier; this log is a second visual check. If one goes stale, the other remains.
Confirm the Hook Actually Fires
Once you add a guard, confirming the guard works is part of the same job. A hook that does not fire can be worse than none, precisely because you believe it is there. To exercise only the hook's decision without pushing anything to the network, invoke a dry-run against a throwaway branch ref.
#!/usr/bin/env bash# verify-prepush.sh — check the pre-push hook rejects off-allowlist remotesset -uo pipefailcheck() { local remote="$1" expect="$2" # never sends; only triggers the hook's decision git push --dry-run "$remote" HEAD:refs/heads/__hook_probe__ >/dev/null 2>&1 local code=$? if [ "$expect" = "allow" ] && [ "$code" -eq 0 ]; then echo "OK $remote was allowed" elif [ "$expect" = "block" ] && [ "$code" -ne 0 ]; then echo "OK $remote was blocked by the hook" else echo "NG $remote expected=$expect actual_code=$code" >&2 return 1 fi}check origin allowcheck mirror block
If origin passes and mirror stops as expected, the hook is doing its job. Because of --dry-run, the check itself never dirties a real remote. I personally run this once whenever I change the hook and whenever I change how the clone workflow runs. Feeling reassured by adding a guard and confirming the guard fires are two different things.
Wrapping Up
The change that made /commit-push-pr target push remotes beyond origin is nothing to worry about on a single remote. But if you keep even a second remote, your push destination can grow from the moment you register it. As a next step, inventory git remote -v and your push-related config once, and check for any pushurl you did not intend. If even one unfamiliar destination turns up, the pre-push hook in this article earns its place. I hope it helps in your own setup — thank you for reading.
Share
Thank You for Reading
Claude Lab is ad-free, supported entirely by members like you. We publish practical guides daily with implementation code, benchmarks, and production-ready patterns. If you've found it useful, we'd love to have you on board.