cloneできたのにpushだけ403になる話

Git

こんにちは、白々さじきです。

今回は、GitHubで「git clone はできるのに git push だけ403で弾かれる」という事象の切り分けと対処法をまとめます。原因は大きく2つ、fine-grained PAT(新しい方の個人アクセストークン)の仕様と、gh コマンドが裏でトークンを差し替えている件です。どちらもUI上はエラーにならないので気付きにくいです。同じところで何度か詰まったので、チェックリストとして残しておきます。

※本記事は、実際に発生した事象をもとにAIを利用して執筆しています。コマンドやエラー内容は実機で確認済みです。

結論

git clone が通ることは、push権限がある証拠になりません。fine-grained PATは「誰が所有するリポジトリか」で挙動が変わり、条件を満たしていないと権限を全部付けても403のままです。継続的に触るリポジトリなら、最初からSSHに寄せてしまうのが結局早いです。

目次

  1. 事象:cloneは通るのにpushだけ403
  2. なぜ紛らわしいのか
  3. 403メッセージで原因を絞る
  4. 先に確認:リポジトリの所有者は個人かOrganizationか
    1. 所有者が個人アカウントの場合:fine-grained PATは使えない
    2. 所有者がOrganizationの場合:ポリシーと管理者承認が要る
  5. もうひとつの原因:ghコマンドが裏でトークンを差し替えている
  6. 切り分け手順
  7. 対処:SSHに寄せる
    1. ghのデフォルトプロトコルも変えておく
    2. GitHubアカウントを複数持っている場合
  8. まとめ

事象:cloneは通るのにpushだけ403

共有されたリポジトリをcloneし、作業ブランチを切ってpushしたところ403で弾かれました。

remote: Write access to repository not granted.
fatal: unable to access 'https://github.com/xxx/yyy.git/': The requested URL returned error: 403

cloneは通っています。なので「アクセス権はあるはず」と考えてしまいました。ここが最初の分岐ミスでした。

なぜ紛らわしいのか

cloneは読み取り、pushは書き込みで、認可が別レイヤーになっています。

さらにpublicリポジトリの場合、cloneは未認証でも通ります。つまりcloneの成功は、そもそも認証が行われたことすら意味しません。「cloneできた」を権限の証拠として扱った時点で、切り分けが1時間分ずれます。

403メッセージで原因を絞る

GitHubの403は、メッセージの文言である程度まで原因を絞れます。ここを最初に見るべきでした。

  • Write access to repository not granted … 認証自体は成功している。トークンに書き込み権限がない
  • Permission to X denied to Y … Y という別アカウントとして認証されている
  • pre-receive hook declined … 権限はある。branch protection等で弾かれている

今回は1番目でした。「認証は通っているが、そのトークンが書き込みを持っていない」状態と確定します。

先に確認:リポジトリの所有者は個人かOrganizationか

fine-grained PATを使っている場合、ここで対処が完全に分かれます。設定画面を触る前に https://github.com/<owner> を開いて、所有者が個人アカウントなのかOrganizationなのかを確認してください。

参考:個人用アクセス トークンを管理する

所有者が個人アカウントの場合:fine-grained PATは使えない

fine-grained PATは、発行者本人が所有するリポジトリしか対象にできません。他人の個人アカウント配下のリポジトリにcollaboratorとして参加している場合、書き込みに使えるのは現状 classic PAT だけです。

厄介なのは、UI上でエラーにならないことです。

  • Repository access で「All repositories」を選んでも、それは自分が所有する全リポジトリの意味
  • 「Only select repositories」の一覧に、collaboratorとして参加しているリポジトリはそもそも出てこない
  • Contents を Read and write にしても、対象が0件なので何も起きない

「全部OKにしたのにダメ」という状況は、この仕様を知らないと解決しません。設定が間違っているのではなく、設定できる場所に対象が存在しない、というのが正確なところです。

所有者がOrganizationの場合:ポリシーと管理者承認が要る

所有者がOrganizationであれば、自分が個人アカウントで招待されている形でも fine-grained PAT 自体は使えます。ただし条件が2つあります。

  • Org側で「Allow access via fine-grained personal access tokens」が有効になっている
  • Org管理者がそのトークンを承認している(Organization settings > Personal access tokens > Pending requests)

ここが落とし穴で、承認待ちの間は権限を全部付けていても同じ403が出ます。トークン側のメッセージは「権限がない」としか言わないため、承認フローの存在を知らないと延々と設定を見直すことになります。

自分側で確認するなら、Settings > Developer settings > Personal access tokens > Fine-grained tokens から該当トークンを開き、対象Orgのステータスが Pending のままになっていないかを見てください。Pendingであれば、あとは管理者に承認を依頼するしかありません。

もうひとつの原因:ghコマンドが裏でトークンを差し替えている

PAT側の設定が正しくても直らない場合、そもそもそのPATが使われていない可能性があります。

gh auth login を実行すると gh auth setup-git が走り、ghのOAuthトークンがgitのcredential helperとして登録されます。この状態でHTTPS pushすると、自分で発行したPATではなくghのトークンが使われます

gh auth status
git config --get-all credential.helper

gh auth status に出るアカウント名が、リポジトリのメンバーやCollaboratorとして登録されているアカウントと一致しているかを確認してください。仕事用と個人用でGitHubアカウントを分けている場合、ここで別アカウントを掴んでいることが多いです。

切り分け手順

上から順に潰していきます。

# 1. remoteの種別を確認
git remote -v

# 2. 認証に使われているトークンの持ち主を確認
gh auth status
git config --get-all credential.helper

# 3. そのトークンが実際に持っている権限を見る
curl -H "Authorization: Bearer <PAT>" \
  https://api.github.com/repos/<owner>/<repo> | jq '.permissions'

3番目で "push": false が返るなら、トークン側の問題で確定です。ローカルのgit設定をいじっても直りません。

対処:SSHに寄せる

classic PATを再発行しても直りますが、有効期限の管理が発生しますし、repo スコープは対象が広いです。Org側の承認待ちに引っかかることもありません。継続的に触るリポジトリならSSHが素直だと思います。

# 鍵の有無を確認
ls -l ~/.ssh/

# なければ生成し、公開鍵を GitHub > Settings > SSH and GPG keys に登録
ssh-keygen -t ed25519 -C "your@example.com"

# 疎通確認(表示されるusernameが目的のアカウントか必ず見る)
ssh -T git@github.com

# 既存リポジトリのremoteを張り替え
git remote set-url origin git@github.com:<owner>/<repo>.git

git push -u origin <branch>

ssh -T で返ってくる Hi <username>! のusernameが、想定しているアカウントかどうかをここで必ず見てください。ここが違っていると、SSHに寄せても結局同じ403を踏みます。

参考:SSH を使用したGitHubへの接続

なお、OrganizationによってはSSH鍵にSSO認可が必要な場合があります。その場合は Settings > SSH and GPG keys の該当鍵から「Configure SSO」で対象Orgを認可してください。

ghのデフォルトプロトコルも変えておく

gh repo clone はデフォルトでHTTPSを使います。ここを変えないと、新しいリポジトリを取ってくるたびに同じ問題を踏みます。

gh config set -h github.com git_protocol ssh
gh config get -h github.com git_protocol   # 確認

GitHubアカウントを複数持っている場合

~/.ssh/config でホスト別に鍵を固定します。

Host github.com
  HostName github.com
  User git
  IdentityFile ~/.ssh/id_ed25519_work
  IdentitiesOnly yes

IdentitiesOnly yes は入れておいてください。これがないとssh-agentが持っている鍵を上から順に提示するため、意図しないアカウントで認証が通ってしまいます。「なぜか別アカウント扱いになる」系のトラブルは、大体これが原因です。

うまくいかないときは、どの鍵が受理されたかを確認します。

ssh -vT git@github.com 2>&1 | grep -i "offering\|accepted"

まとめ

環境が変わるたびにGitHubの認証設定をやり直すことになるので、チェックリスト化しておきます。

  • リポジトリの所有者は個人アカウントかOrganizationか
  • 自分の権限はWrite以上か(api.github.com/repos/.../permissionspush: true
  • fine-grained PATを使っていないか(他人の個人リポジトリなら使えない)
  • Org所有なら、トークンが承認待ち(Pending)で止まっていないか
  • gh auth status のアカウントは想定通りか
  • SSHに寄せて gh config set -h github.com git_protocol ssh 済みか

git clone が通ることを権限の証拠にしない。これだけ覚えておけば、次に踏んだときの切り分けが速くなります。次は、複数アカウントを前提にした ~/.ssh/config と Git の includeIf による設定分離あたりを整理してみたいと思います。

※本記事は執筆時点(2026年8月)の仕様に基づいています。GitHubのPAT周りの仕様や設定画面は変更される可能性があるため、実際の作業時は公式ドキュメントも併せてご確認ください。

サポートのお願い

下記リンクからお買い物いただけると、ブログ運営のための費用が増え、有料サービスを利用した記事作成が可能になります。ご協力よろしくお願いします!



コメント

タイトルとURLをコピーしました