
プライベートリポジトリで git fetch origin が 403 になる — URL埋め込みPATの落とし穴と対策
はじめに
GitHub の Organization プライベートリポジトリで CI/CD スクリプトやリリース自動化を組んでいると、こんなエラーに遭遇することがあります。
remote: Write access to repository not granted.
fatal: unable to access 'https://github.com/YOUR-ORG/your-repo.git/': The requested URL returned error: 403
git push は成功するのに、直後の git fetch origin で 403。一見矛盾したこの挙動、原因は PAT(Personal Access Token)を URL に埋め込む push 方式 と、認証情報を持たない origin リモート の組み合わせにありました。
この記事では、なぜこの問題が起きるのか、そしてプライベートリポジトリで安全に git 操作を行うための $AUTHED_ORIGIN パターンを紹介します。
前提環境
- GitHub Organization のプライベートリポジトリ
- PAT は 1Password で管理(git config にトークンを保存しない運用)
ghCLI の Organization 用自作ラッパー関数(gh-org)でトークンを取得(後述)- macOS / zsh
何が起きているのか
典型的な push スクリプト
Organization リポジトリへの push では、git push origin main が認証エラーで失敗するため、PAT を URL に埋め込んで push する方式を採用していました。
source ~/.zshrc
GH_TOKEN=$(gh-org auth token)
AUTHED_ORIGIN=$(git remote get-url origin | sed "s|https://|https://YOUR_USERNAME:${GH_TOKEN}@|")
git push "$AUTHED_ORIGIN" main
YOUR_USERNAME は自分の GitHub ユーザー名に置き換えてください。この方式自体は問題なく動きます。しかし、URL ベースの push には ローカルのトラッキング ref が更新されない という副作用があります。push 後に git status を実行すると、push が成功しているのに "Your branch is ahead of 'origin/main' by N commits" と表示されてしまいます。
fetch で ref を更新しようとして 403
この副作用を解消するために、push 直後に git fetch を実行していました。
git push "$AUTHED_ORIGIN" main && git fetch origin # ← ここで 403
git push は $AUTHED_ORIGIN(トークン入り URL)を使うので成功。しかし git fetch origin は git config に登録されたリモート URL をそのまま使います。
# origin のリモート URL(トークンなし)
https://github.com/YOUR-ORG/your-repo.git
パブリックリポジトリなら認証なしでも fetch できますが、プライベートリポジトリでは読み取りにも認証が必要。結果、403 が返ります。
問題の図解

解決策: すべての git 操作で $AUTHED_ORIGIN を使う
修正はシンプルです。origin を使っていた箇所をすべて $AUTHED_ORIGIN に統一します。
source ~/.zshrc
GH_TOKEN=$(gh-org auth token)
AUTHED_ORIGIN=$(git remote get-url origin | sed "s|https://|https://YOUR_USERNAME:${GH_TOKEN}@|")
# push も fetch も同じ認証済み URL を使う
git push "$AUTHED_ORIGIN" main && git fetch "$AUTHED_ORIGIN"
pull / fetch --prune も同様
リリーススクリプトなどで pull や prune を実行する場合も同じです。
# NG: origin はトークンなし → 403
git pull origin main && git fetch --prune origin
# OK: 認証済み URL を一貫して使用
git pull "$AUTHED_ORIGIN" main && git fetch --prune "$AUTHED_ORIGIN"
なぜ origin にトークンを設定しないのか
「git remote set-url origin でトークン入り URL を設定すればいいのでは?」と思うかもしれません。しかし、以下の理由からこの方法は避けています。
| 方式 | メリット | デメリット |
|---|---|---|
git remote set-url でトークン埋め込み |
以降の操作で origin がそのまま使える |
トークンが .git/config に平文で残る |
環境変数 $AUTHED_ORIGIN |
トークンがファイルに残らない | すべての操作で $AUTHED_ORIGIN を明示する必要がある |
| git credential helper | origin がそのまま使える、トークンもキーチェーンで管理 |
Organization PAT と個人 PAT の使い分けが煩雑 |
1Password で管理している Organization PAT を毎回取得して使い捨てる $AUTHED_ORIGIN 方式は、セキュリティ面では最も安全です。トレードオフとして、スクリプト内の origin を $AUTHED_ORIGIN に統一する手間が必要になります。
&& チェーンの罠
もうひとつ気づきにくい問題があります。git fetch origin が 403 で失敗すると、&& チェーンの後続コマンドも実行されません。
# fetch が 403 で失敗 → tags の push が実行されない
git push "$AUTHED_ORIGIN" main && git fetch origin && git push "$AUTHED_ORIGIN" --tags
実際にこの問題に遭遇したとき、「push は成功したのにタグが push されていない」という状況になりました。エラーメッセージは git fetch origin の 403 を報告していましたが、タグが push されていないことには気づきにくい。
対策として、タグの push は fetch と分離するか、fetch を修正して $AUTHED_ORIGIN を使うようにします。
# fetch を修正(推奨)
git push "$AUTHED_ORIGIN" main && git fetch "$AUTHED_ORIGIN"
git push "$AUTHED_ORIGIN" --tags
# または、fetch の失敗を許容する
git push "$AUTHED_ORIGIN" main
git fetch "$AUTHED_ORIGIN" || true
git push "$AUTHED_ORIGIN" --tags
gh CLI ラッパーのトークン取得パターン
参考までに、1Password から Organization PAT を取得する gh-org ラッパーのパターンを紹介します。
# 1Password 管理の Organization PAT で gh を実行するラッパー
gh-org() {
GH_TOKEN=$(op read "op://Private/GitHub Org PAT/token") gh "$@"
}
# トークンの取得
GH_TOKEN=$(gh-org auth token)
# 認証済み URL の構築
AUTHED_ORIGIN=$(git remote get-url origin | sed "s|https://|https://YOUR_USERNAME:${GH_TOKEN}@|")
gh-org auth token は GH_TOKEN 環境変数がセットされた状態で gh auth token を実行するため、1Password から取得した PAT がそのまま返されます。
まとめ
| 状況 | 原因 | 対策 |
|---|---|---|
git fetch origin が 403 |
プライベートリポジトリで認証なし fetch | $AUTHED_ORIGIN を使う |
push 後の git status が "ahead" |
URL ベース push はトラッキング ref を更新しない | push 後に git fetch "$AUTHED_ORIGIN" |
&& チェーンで後続が実行されない |
途中の fetch 失敗でチェーンが中断 | fetch を修正するか、分離する |
ポイントは 「プライベートリポジトリでは読み取りにも認証が必要」 という当たり前の事実です。git push だけ認証済み URL を使い、git fetch は素の origin を使うという非対称な構成が、この問題の根本原因でした。
PAT を URL に埋め込む方式を採用する場合は、push / fetch / pull すべてで一貫して $AUTHED_ORIGIN を使うことを徹底しましょう。




