夜間の自動処理を終えたあと、いつものように push のログを目で追っていて、行が一本多いことに気づきました。origin への push はいつも通り。その下に、見覚えのあるようで少し身構える別のリモート名が並んでいます。私はそのリモートを、手元で参照するためだけに登録していたつもりでした。押すつもりはなかったのに、押されている。数字も差分も正しいのに、届いた先が想定より一つ多い。この小さな余分こそ、7月14日の更新がもたらした挙動の変化でした。
/commit-push-pr は、これまで origin への push を前提に動いていました。更新後は、origin に加えて設定済みの push リモートへの push も自動で許可されます。単一リモートで運用しているなら、この変更はまず表に出ません。けれど個人開発で複数サイトを扱い、ミラーやバックアップ用の第二リモートを一つでも登録していると、意図しない push 先が静かに増えます。今日はこの変化を、私自身の夜間運用に引きつけて棚卸しし、押してよいリモートだけに絞る設計へ落とし込んだ記録です。
push が空振りして「成功したのに何も届かない」側の落とし穴はpushは成功と出るのに何も反映されない現象で扱いました。本稿は逆に、押すつもりのない先へ「余分に届いてしまう」側を防ぎます。
origin だけを見ていると、増えた push 先を見落とす
これまで push 先を意識せずに済んでいたのは、多くのリポジトリで push 先が origin ただ一つだったからです。コミットして押す、その一往復のあいだにリモートが一つしかなければ、どこへ届くかを考える必要はありません。
今回の変更は、その前提を静かにずらします。/commit-push-pr が「origin に加えて設定済みの push リモート」を対象にするということは、リモートを追加した時点で push の宛先が増え得る、という意味です。しかもそれは、あなたが明示的に「このリモートにも押して」と言った結果ではなく、コマンドの既定の振る舞いとして起こります。
厄介なのは、リモートの登録は push とは別の理由で行われることが多い点です。他人のフォークを取り込むため、別環境のミラーを参照するため、あるいは単に差分を眺めるため。どれも「読む」目的で足したリモートが、いつの間にか「書く」対象に化けていた、という筋書きです。
いま自分のリポジトリがどのリモートへ push し得るか
対策の前に、現状を正確に知る必要があります。まず基本は次の一行です。
git remote -v
ここで見落としがちなのが、fetch と push の URL が別々に表示される点です。同じリモート名でも、取得先と送信先が違う設定になっている場合があります。
origin https://github.com/you/site.git (fetch)
origin https://github.com/you/site.git (push)
mirror https://github.com/you/site.git (fetch)
mirror git@backup.example:you/site.git (push)
この mirror のように push URL だけ別の宛先を向いているリモートは、まさに「押されると困る先」になりがちです。さらに、リモートの登録そのものより上位で push 先を決める設定もあります。棚卸しの対象を整理すると、次の三つを押さえておけば見落としは減ります。
| 設定 | 役割 | 確認コマンド |
| remote.<name>.pushurl | そのリモートの push だけ別 URL へ向ける | git config --get-all remote.mirror.pushurl |
| remote.pushDefault | push 時の既定リモートを origin 以外に変える | git config --get remote.pushDefault |
| branch.<name>.pushRemote | ブランチ単位で push 先を上書きする | git config --get branch.main.pushRemote |
これらが未設定なら push は素直に origin へ向かいます。逆に一つでも設定されていると、コマンドの見た目からは push 先を推測できません。私はこの三つをまとめて吐き出す一行を、リポジトリ確認の定型にしています。
git config --get-regexp '^(remote\.|branch\..*\.pushremote)' | grep -i 'push\|url'
「設定済みの push リモート」が意図せず増える典型パターン
push 先が増える経路は、だいたい決まっています。実際に個人開発で踏みやすい順に並べます。
- バックアップ用ミラーの登録 — 別サービスへ複製を残すために
mirror を足したが、pushurl まで設定していて書き込み可能になっている。
- フォーク元 upstream の取り込み — upstream を fetch 専用のつもりで登録したのに、push URL が空でなく、宛先として拾われてしまう。
- 一時的な検証リモート — 別環境へ試し push したあと
git remote remove を忘れ、登録が残り続けている。
いずれも「読むために足した」「消し忘れた」という、悪意もミスの自覚もない登録です。だからこそコマンドが対象を広げたとき、静かに押されます。ここで大切なのは、リモートを一つ残らず消すことではありません。参照用のリモートは参照用のまま残しつつ、push だけを許可した先に限定する。この切り分けができれば、利便性を捨てずに事故だけを防げます。
意図しない push を止める三つの層
守りは一枚では足りません。私は次の三層で考えています。
第一層は棚卸しです。前節のコマンドで push し得るリモートを洗い出し、意図しない pushurl があれば外します。読むだけでよいリモートには、push URL に無効値を割り当てて書き込みを封じる手もあります。
第二層はフックです。棚卸しは一度きりでは陳腐化するため、push のたびに機械的に許可リストと照合する仕組みを挟みます。これが本稿の中心です。
第三層は実行前の確認です。無人実行では対話の確認が使えないぶん、どのリモートへ押す設定になっているかを、処理の冒頭でログに残します。あとからログを読んだときに、押し先が想定通りだったかを検証できる状態にしておきます。
pre-push フックで許可リスト外のリモートを止める
pre-push フックは、push が実際にネットワークへ出る直前に呼ばれ、終了コードが 0 以外なら push そのものを中断します。ここに許可リストの照合を置けば、コマンドが宛先を広げても最後の一線で止められます。
まず、フックを入れる前の素の状態はこうです。どのリモートへ押しても素通りします。
# Before: フックなし。/commit-push-pr が対象にした全リモートへそのまま push される
$ git push mirror main
# → 何の確認もなく backup.example へ到達する
フックを入れた後は、許可リストにないリモート名を弾きます。.git/hooks/pre-push に次を置き、実行権を付けます。Git は push 先のリモート名を第一引数で渡してくれます。
#!/usr/bin/env bash
# .git/hooks/pre-push
# 許可したリモートにだけ push を通す。それ以外は中断する。
set -euo pipefail
ALLOWED="origin" # 空白区切りで複数指定可: "origin release"
remote_name="$1" # push 先のリモート名
is_allowed=0
for r in $ALLOWED; do
if [ "$remote_name" = "$r" ]; then
is_allowed=1
break
fi
done
if [ "$is_allowed" -ne 1 ]; then
echo "pre-push: blocked push to '$remote_name' (allowed: $ALLOWED)" >&2
echo "pre-push: 許可リストに追加する場合は ALLOWED を見直してください" >&2
exit 1
fi
exit 0
$ chmod +x .git/hooks/pre-push
# After: 許可外リモートへの push はフックが中断する
$ git push mirror main
pre-push: blocked push to 'mirror' (allowed: origin)
error: failed to push some refs to '...'
この方式の利点は、/commit-push-pr が内部でどのリモートを対象にしようと、最終的な出口を一箇所で押さえられることです。コマンドの挙動が将来また変わっても、許可リストという判断基準はこちら側に残ります。落とし穴も一つあります。.git/hooks はクローンに含まれないため、リポジトリを clone し直すたびにフックは失われます。夜間に使い捨てのクローンで push する運用では、この点が本番運用でそのまま事故につながりました。対処として、私は clone 直後にフックを配置する処理を push 前の定型手順へ組み込むことを推奨します。core.hooksPath を共有ディレクトリに向け、フック本体をリポジトリ管理下に置くのも有効です。
無人・スケジュール実行での注意
対話セッションなら、余分なリモートへ押しても「あれ、宛先が多い」と気づく余地があります。無人実行にはその余地がありません。確認プロンプトは出ないか、出ても答える人がいません。だからこそ、押し先の正しさは実行前に構造で担保しておく必要があります。
私が定型にしているのは、処理の冒頭で「これから push し得るリモート」を一覧としてログに刻むことです。
echo "== push targets check =="
git config --get remote.pushDefault || echo "pushDefault: (none, origin)"
git remote -v | awk '$3=="(push)"{print $1, $2}'
この数行があるだけで、あとからログを読んだときに「押し先は origin だけだったか」を一目で確かめられます。フックが第一の防壁、このログが第二の目視確認、という二段構えです。片方が陳腐化しても、もう片方が残ります。
フックが本当に効くかを自分で確かめる
守りを入れたら、守りが効いているかを確かめるまでが一組です。効かないフックは、あると思い込んでいるぶん、無いより危険なことすらあります。ネットワークへ実際に押さずにフックの判定だけを試すには、存在しないブランチ範囲を渡して dry-run で呼び出します。
#!/usr/bin/env bash
# verify-prepush.sh — pre-push フックが許可外リモートを弾くか検証する
set -uo pipefail
check() {
local remote="$1" expect="$2"
# 実際には送らず、フックの判定のみを起動する
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 は許可されました"
elif [ "$expect" = "block" ] && [ "$code" -ne 0 ]; then
echo "OK $remote はフックが中断しました"
else
echo "NG $remote 期待=$expect 実際コード=$code" >&2
return 1
fi
}
check origin allow
check mirror block
期待した通りに origin が通り mirror が止まれば、フックは意図通りに働いています。--dry-run なので、この検証自体が実リモートを汚すことはありません。私自身はこの検証を、フックを更新したときとクローン運用を変えたときに必ず一度回すようにしています。守りを足した安心と、守りが効いている確認とは、別物だからです。
まとめ
/commit-push-pr が origin 以外の push リモートも対象にするようになった変更は、単一リモートなら気にする必要はありません。ですが第二のリモートを一つでも持つなら、push の宛先は登録した瞬間から増え得ます。次の一手として、まず git remote -v と push 系設定の棚卸しを一度だけ行い、意図しない pushurl がないかを確かめてみてください。そこで一つでも見覚えのない宛先が見つかったら、この記事の pre-push フックを置く価値があります。実装の参考になれば幸いです。お読みいただきありがとうございました。