
WSL の pass に保存した GitHub PAT を更新する手順と、pinentry が詰まったときの対処
記事タイトル:WSL の pass に保存した GitHub PAT を更新する手順と、pinentry が詰まったときの対処
記事URL:https://dev.classmethod.jp/articles/wsl-pass-github-pat-pinentry-timeout/
コーヒーが好きな emi です。
GitHub の Personal Access Token(PAT)の有効期限が切れたため、有効期限を延長しました。あわせてアクセス対象のリポジトリを 1 つ追加したところ、新しい PAT の文字列が発行されました。
私の WSL 環境では、Git Credential Manager(GCM)の認証情報を pass に保存しています。この構成に関する情報は以下記事を参照してください。

Git Credential Manager(GCM)、pass、GPG の関係
- 💁 Git Credential Manager(GCM)= 銀行の窓口係
Git 用のトークン保管・管理ツール。Git から「GitHub のトークンが欲しい」と頼まれたときに、保管庫まで取りに行ってくれる窓口係のような存在です。一度認証に成功すると、次回以降は保管しておいたトークンを自動で使ってくれるので、毎回パスワードやトークンを入力しなくて済むようになります。 - 🏦
pass= 金庫室(保管場所)
暗号化されたファイルを~/.password-store/というディレクトリにまとめて保管する、シンプルなパスワード管理ツールです。トークンを含むファイルが並んでいる「金庫室」そのものにあたります。ただし、中に置かれているファイルはすべて暗号化されているので、金庫室に入っただけでは中身を読むことはできません。 - 🔑 GPG (GNU Privacy Guard) = 金庫を開ける鍵
ファイルの暗号化・復号を行うツールです。passに保管されているファイルは GPG で暗号化されているため、中身を取り出すには GPG による復号が必要になります。そして GPG の鍵を使うときには、鍵を開けるための合言葉である パスフレーズ の入力が求められます。
そのため、これまで使っていた PAT を新しい文字列へ更新し、新しく追加したリポジトリの認証情報も登録する必要があります。
作業自体は git credential-manager store で更新できましたが、途中でコマンドが無反応になり、pinentry の白いウィンドウが表示されるトラブルにも遭遇しました。PAT の更新手順と、そこで詰まった内容をまとめます。
動作環境
| 項目 | バージョン、設定 |
|---|---|
| Windows | Windows 11(10.0.26100) |
| ディストリビューション | Ubuntu 22.04.5 LTS |
| Git Credential Manager | pass バックエンドを使用 |
| pinentry | pinentry-gtk2 1.1.1 |
| GPG エージェント | パスフレーズを 8 時間キャッシュ |
useHttpPath = true を図で理解する
私の環境では ~/.gitconfig に useHttpPath = true を設定しています。
[user]
name = emi-ki
email = 86865300+emi-ki@users.noreply.github.com
[core]
sshCommand = /usr/bin/ssh
[credential]
helper =
helper = /usr/local/bin/git-credential-manager
credentialStore = gpg
[credential "https://dev.azure.com"]
useHttpPath = true
[credential "https://github.com"]
useHttpPath = true
useHttpPath = false(デフォルト)の場合
ホスト単位でまとめて 1 件だけ保存されます。

ここでいう「ホスト」
ここでいう「ホスト」は、接続先 URL の github.com の部分です。Git の認証情報を保存する仕組みは GitHub 専用ではなく、GitLab や GitHub Enterprise Server など、ほかの接続先でも使われます。そのため、仕組みを説明するときは今回の接続先である github.com ではなく、それらをまとめて「ホスト」と呼びます。
PAT を更新するときは 1 か所直せば全リポジトリに反映されます。一方で、リポジトリごとに違うトークンを使い分けることはできません。
ここで補足しておくと、PAT が漏洩したときの被害範囲を決めるのは PAT 側の scope とアクセス対象リポジトリの設定で、pass の保存単位ではありません。useHttpPath = true にしてエントリが分かれても、そこに同じ PAT を入れているなら被害範囲は変わりません。リポジトリごとに別の PAT を発行して初めて分離になります。
useHttpPath = true の場合(今回の環境)
useHttpPath = true を設定していると、pass の保存先がリポジトリのパスごとに分かれます。

PAT 自体は 1 本で複数リポジトリをカバーしていますが、GCM は認証情報を github.com で一括保存せず、URL のパスごとに別のエントリとして pass に保存します。
つまり pass ls で見ると、以下のような構造になります。
git
└── https
└── github.com
└── <組織Org>
├── repository-a.git
│ └── emi-ki
├── repository-b.git
│ └── emi-ki
└── repository-c
└── emi-ki
末端に来るのはリポジトリ名ではなく ユーザー名 です。GCM は 名前空間/プロトコル/ホスト/パス/ユーザー名 の順でエントリを作るため、リポジトリ名はその手前のディレクトリになります。同じリポジトリに複数のユーザー名で認証情報を保存した場合は、同じ階層にユーザー名が並びます。
URL のどこまでを使って認証情報を区別するのか
useHttpPath の違いは、GCM が認証情報を探すときに、接続先 URL のどこまでを使って照合するかです。useHttpPath = false では https://github.com までですが、true ではその後ろにある <組織Org>/repository-b.git のようなパスも含まれます。同じ github.com への接続でも、パスが異なれば別の認証情報として扱われます。

今回は、新しい PAT を使う 3 つのリポジトリをまとめて更新します。3 つとも private リポジトリです。
pass に保存されているエントリを確認する
最初に、現在のエントリを確認しました。
pass ls git
出力のうち、今回関係する部分を抜粋すると次のようになっていました。
emiki@<hostname>:~/work$ pass ls git
git
└── https
└── github.com
└── <組織Org>
├── repository-a.git
│ └── emi-ki
└── repository-c
└── emi-ki
emiki@<hostname>:~/work$
この時点では repository-b.git のエントリはまだありません。今回アクセス対象に追加したリポジトリなので、あとで新規に作成します。
ここで注意したいのが、remote URL の末尾にある .git の有無です。GitHub ではどちらも同じリポジトリを指しますが、useHttpPath = true の環境では、GCM と pass は別の認証情報として扱います。PAT を保存するときは、ローカルリポジトリの origin に .git が付いているかを次のコマンドで確認します。
git -C <ローカルリポジトリのパス> remote get-url origin
https://github.com/<組織Org>/repository-a.git と表示された場合は、GCM に渡す path にも .git を付けて <組織Org>/repository-a.git とします。.git のない URL が表示された場合は、path にも .git を付けません。
remote URL と .git の有無について
たとえば、次のコマンドで GitHub のリポジトリをクローンしたとします。
git clone https://github.com/<組織Org>/repository-a.git
クローン後のローカルリポジトリで git fetch や git push を実行すると、Git は同じ GitHub リポジトリへ接続します。そのため、Git はクローンに使った URL をローカルリポジトリ内に記録しています。
以下は、repository-a ディレクトリに保存されている URL を確認するコマンドです。-C repository-a は、repository-a ディレクトリへ移動してから Git コマンドを実行する、という意味です。
git -C repository-a remote -v
実行結果は次のようになります。
origin https://github.com/<組織Org>/repository-a.git (fetch)
origin https://github.com/<組織Org>/repository-a.git (push)
この origin は、GitHub 側のリポジトリを指すために Git が付けた名前です。そして、origin の横に表示されている https://github.com/<組織Org>/repository-a.git が remote URL です。Git は git fetch や git push の際に、この URL を使って GitHub へ接続します。
GitHub は、末尾に .git が付く URL と付かない URL のどちらでも同じリポジトリへ接続できます。
https://github.com/<組織Org>/repository-a.git
https://github.com/<組織Org>/repository-a
.git は Git リポジトリであることを示すために使われてきた表記ですが、GitHub は .git がなくても接続先を判別できるため、省略できます。リポジトリが 2 つ存在するわけではなく、同じリポジトリを指す 2 通りの書き方です。クローンしたときに使った方の URL が、そのまま origin の remote URL として保存されます。
Git Credential Manager 経由で PAT を更新する
PAT はコマンド履歴へ残したくないため、read で変数に入れます。
read -rsp 'PAT: ' PAT; echo
-s を付けているので、入力した文字列は画面に表示されません。
続けて、今回の PAT を使うリポジトリのエントリを更新します。
for p in <組織Org>/repository-a.git \
<組織Org>/repository-c \
<組織Org>/repository-b.git ; do
echo "=== $p ==="
printf 'protocol=https\nhost=github.com\npath=%s\nusername=emi-ki\npassword=%s\n\n' "$p" "$PAT" \
| git credential-manager store
echo "exit=$?"
done
unset PAT
printf では、GCM が受け取れる形式で接続先やユーザー名、PAT を組み立てています。最後に空行を入れるところまでが入力です。
その内容を git credential-manager store に渡すと、GCM が pass のエントリを保存します。同じ protocol、host、path、username の組み合わせがすでにあれば上書きし、なければ新しく作成します。
今回、新しく追加したリポジトリは repository-b.git だけです。しかし、pass の各エントリには PAT への参照ではなく、PAT の文字列そのものが保存されています。今回は PAT の設定変更後に新しい文字列が発行されたため、既存の repository-a.git と repository-c に保存されている文字列も古くなりました。そのため、この 2 エントリを新しい PAT で上書きしながら、repository-b.git のエントリを新しく作成しています。
PAT の設定を変更しても文字列が変わらなければ、pass の更新は不要です。新しい文字列が発行された場合は、useHttpPath = true の環境では、その PAT を使うエントリをそれぞれ更新する必要があります。
毎回対象を並べるのは面倒なので、今後はリポジトリごとに PAT を発行しようかとも考えましたが、リポジトリがどんどん増えていくことを考えると運用管理負荷が高くなって気になります。
どう運用するのがいいのか、まだ答えは出ていないのですが、今回は一旦この形で進めてみました。
最後の unset PAT は、シェル変数に入れた PAT を消すためのものです。
pass insert を直接使わない理由
今回、pass は認証情報の保存先として使っていますが、Git が認証情報を読み書きするときの窓口は GCM です。GCM は PAT だけでなく、ユーザー名などのメタデータも決まった形式で pass に保存します。
まず、保存されているファイルの中身を確認します。1 行目に PAT が入っているので、tail で 2 行目以降だけを表示しました。
pass show git/https/github.com/<組織Org>/repository-a.git/emi-ki | tail -n +2
実行結果
emiki@<hostname>:~/work$ pass show git/https/github.com/<組織Org>/repository-a.git/emi-ki | tail -n +2
service=https://github.com/<組織Org>/repository-a.git
account=emi-ki
ファイルは 3 行あります。1 行目が PAT、2 行目が service=、3 行目が account= です。service= には remote URL がそのまま入っていて、.git の有無もここに現れます。
では、この 2 行が無いと GCM はどうなるのか。実在しないホスト名でダミーのエントリを作って確かめました。本物の認証情報には触れていません。
まず pass insert で、秘密の文字列だけを書き込みます。
printf 'dummy-secret-A\n' | pass insert -m -f git/https/dummy.example.com/dummy-user
このエントリを GCM に読ませます。GCM_INTERACTIVE=never は、認証情報が見つからなかったときに GCM が新しい入力を求めてこないようにするためのものです。
printf 'protocol=https\nhost=dummy.example.com\nusername=dummy-user\n\n' | GCM_INTERACTIVE=never git credential-manager get
実行結果
emiki@<hostname>:~/work$ printf 'protocol=https\nhost=dummy.example.com\nusername=dummy-user\n\n' | GCM_INTERACTIVE=never git credential-manager get
fatal: Cannot prompt because user interactivity has been disabled.
保存したはずの dummy-secret-A は返ってきません。GCM はエントリを見つけられず、新しく入力させようとして止まっています。pass ls にも pass show にも出てくるのに、GCM から見ると無いのと同じ扱いです。ファイルはあるのに認証だけ通らないので、原因の分かりにくい壊れ方をします。
次に、GCM と同じ 3 行形式で書いた場合です。
printf 'dummy-secret-B\nservice=https://dummy2.example.com\naccount=dummy-user\n' | pass insert -m -f git/https/dummy2.example.com/dummy-user
実行結果
emiki@<hostname>:~/work$ printf 'protocol=https\nhost=dummy2.example.com\nusername=dummy-user\n\n' | GCM_INTERACTIVE=never git credential-manager get
protocol=https
host=dummy2.example.com
username=dummy-user
password=dummy-secret-B
こちらは読めました。保存場所も GPG 鍵も同じで、違うのは中身の 2 行だけです。
最後に、既存のエントリを pass edit で 1 行目だけ書き換えた場合も試しました。エディタの代わりに sed を渡して、1 行目を置き換えています。
EDITOR="sed -i 1s/.*/dummy-secret-C2/" pass edit git/https/dummy3.example.com/dummy-user
実行結果
emiki@<hostname>:~/work$ pass show git/https/dummy3.example.com/dummy-user
dummy-secret-C2
service=https://dummy3.example.com
account=dummy-user
emiki@<hostname>:~/work$ printf 'protocol=https\nhost=dummy3.example.com\nusername=dummy-user\n\n' | GCM_INTERACTIVE=never git credential-manager get
protocol=https
host=dummy3.example.com
username=dummy-user
password=dummy-secret-C2
service= と account= を残したまま 1 行目だけ直せば、GCM は新しい値を返します。つまり既存 PAT の更新に限れば、直接編集でも動きます。
それでも git credential-manager store を使ったのは、新規作成と更新を区別しなくて済むからです。今回は repository-b.git が新規で、残り 2 つが更新でした。pass 側でやるなら、新規は 3 行を自分で書き、更新は 1 行目だけ直す、という使い分けが要ります。GCM に渡す形はどちらも同じです。
pass edit はエディタが開くので、2 行目以降をうっかり触れば読めなくなります。その状態が最初のダミーの結果です。書き込みを GCM に任せておけば、この事故は起きません。
なお、ダミーのエントリは確認後に pass rm で削除しています。実行結果からは、存在しないホスト名を使ったことで出る接続先判定の警告を省いています。
git credential-manager store と平文保存の store は別物
git credential-manager store は、git-credential-manager という GCM のプログラムに store 処理を実行させるコマンドです。今回の環境では credentialStore = gpg を設定しているため、GCM は認証情報を GPG で暗号化して pass に保存します。
一方、Git の平文保存用ヘルパーは credential.helper = store と設定した場合に使われる git credential-store です。名前に同じ store が含まれていますが、今回実行した git credential-manager store とは別のコマンドです。
実際に今回の登録では GPG と pinentry が起動し、pass のエントリが更新されました。これは GCM の保存先として gpg が使われたためです。
参考資料
git credential-manager store が無反応になった
ここまでならすぐに終わるはずでしたが、実際にループを実行すると、最初のエントリで止まってしまいました。エラーは出ず、プロンプトも戻ってきません。

今回 PAT の保存処理の途中で止まったため、GCM、GPG、pinentry のどこかが応答を待っているのではないかと考えました。そこで別のターミナルを開き、関係するプロセスが残っていないか確認しました。
ps -ef | grep -E 'pinentry|gpg-agent|credential' | grep -v grep
このときは、次のプロセスが残っていました。
emiki@<hostname>:~/work$ ps -ef | grep -E 'pinentry|gpg-agent|credential' | grep -v grep
emiki 212 15 0 05:48 ? 00:00:00 gpg-agent --homedir /home/emiki/.gnupg --use-standard-socket --daemon
emiki 214 212 0 05:48 ? 00:00:00 pinentry-gtk-2 --display :0
emiki 1838 883 0 07:47 pts/1 00:00:00 git credential-manager store
emiki 1839 1838 0 07:47 pts/1 00:00:00 /usr/local/bin/git-credential-manager store
emiki@<hostname>:~/work$
git credential-manager store を実行したのは 07:47 ですが、pinentry-gtk-2 は 05:48 から残っています。以前に起動した pinentry が応答待ちのまま居座り、新しい GPG の処理も待たされているようでした。
私の環境では、GPG のパスフレーズ入力に GUI 版の pinentry-gtk2(実行ファイル名は pinentry-gtk-2)を使っています。普段は WSLg の別ウィンドウに入力画面が表示されますが、このときはウィンドウが真っ白になり、入力欄が描画されませんでした。

pinentry を直接実行してみても、ウィンドウは白いままタイムアウトしました。
echo -e 'SETDESC test\nGETPIN' | /usr/bin/pinentry-gtk-2
▼実行結果
emiki@<hostname>:~/work/repository-b$ echo -e 'SETDESC test\nGETPIN' | /usr/bin/pinentry-gtk-2
OK Pleased to meet you
OK
S ERROR gtk2.? 83886142
ERR 83886142 Timeout <Pinentry>
emiki@<hostname>:~/work/repository-b$
WSLg の状態を確認する
WSLg が使う X11 ソケットも確認しました。
ls -l /tmp/.X11-unix/
srwxrwxrwx 1 emiki emiki 0 Aug 8 16:51 X0
作業したのは 8 月 10 日でしたが、X0 の日時は 8 月 8 日のままです。これだけで原因を断定はできませんが、WSLg のセッションが長時間残り、描画状態がおかしくなっていることが考えられました。
以前、Claude Code の TUI と pinentry-curses が衝突したため、GUI 版の pinentry-gtk2 に切り替えています。今回のためにターミナル版へ戻すのではなく、まず WSLg の状態をリセットすることにしました。
wsl --shutdown で白いウィンドウを解消する
実行中の作業を保存して WSL のターミナルを閉じたあと、Windows 側の PowerShell で次のコマンドを実行しました。
wsl --shutdown
このコマンドを実行すると、起動中の WSL ディストリビューションがすべて停止します。別の WSL 環境で作業中のプロセスがある場合も終了するため、実行前に保存しておいた方がよいです。
WSL を開き直して X11 ソケットを確認すると、X0 の日時が新しくなっていました。
ls -l /tmp/.X11-unix/
srwxrwxrwx 1 emiki emiki 0 Aug 10 07:55 X0
続けて GPG の署名を実行し、pinentry の表示を確認しました。
echo test | gpg --clearsign > /dev/null && echo "GPG OK"
今度は入力欄のあるダイアログが正常に表示され、パスフレーズを入力すると GPG OK が返りました。白いウィンドウも解消しています。

PAT をあらためて登録する
WSLg の復旧後に、先ほどの git credential-manager store のループをもう一度実行しました。今度は各リポジトリで GPG の暗号化処理が進み、すべて exit=0 になりました。成功です。
登録後のエントリも確認します。
pass ls git
新しく追加した repository-b.git を含め、今回の PAT を使う 3 リポジトリが表示されました。
ls-remote で認証を確認する
PAT が正しく使えるかどうかは、リポジトリをクローンしなくても git ls-remote で確認できます。
for r in repository-a.git \
repository-c \
repository-b.git ; do
git ls-remote "https://github.com/<組織Org>/$r" HEAD >/dev/null 2>&1 \
&& echo "OK $r" \
|| echo "NG $r"
done
3 リポジトリとも OK になり、ユーザー名や PAT の入力も求められませんでした。pass に保存した新しい PAT で認証できています。
なお、public リポジトリは未認証でも git ls-remote が通るため、この確認方法が意味を持つのは private リポジトリの場合です。今回の 3 つはいずれも private なので、OK は認証が通ったことの確認になります。
今回は追加した repository-b リポジトリを実際にクローンして、こちらでも認証できることを確認しました。
git clone https://github.com/<組織Org>/repository-b.git
▼実行結果
emiki@<hostname>:~/work/repository-b$ git clone https://github.com/<組織Org>/repository-b.git
Cloning into 'repository-b'...
remote: Enumerating objects: 2450, done.
remote: Counting objects: 100% (678/678), done.
remote: Compressing objects: 100% (325/325), done.
remote: Total 2450 (delta 390), reused 584 (delta 330), pack-reused 1772 (from 1)
Receiving objects: 100% (2450/2450), 19.57 MiB | 26.68 MiB/s, done.
Resolving deltas: 100% (1483/1483), done.
emiki@<hostname>:~/work/repository-b$
.git なしで作った重複エントリを削除する
作業の途中では、repository-b の remote URL に .git が付くか確信が持てなかったため、.git ありとなしの両方を一度登録しました。
クローン後に remote URL を確認します。
git -C ~/work/repository-b-test/repository-b remote get-url origin
https://github.com/<組織Org>/repository-b.git
実際に使われるのは .git ありのエントリでした。不要になった .git なしのエントリを削除します。
pass rm git/https/github.com/<組織Org>/repository-b/emi-ki
削除後も git fetch が通ることを確認しました。
git -C ~/work/repository-b-test/repository-b fetch
認証を求められずに完了したため、必要なエントリだけが残っている状態です。
おわりに
GitHub で PAT を更新し、pass に新しい PAT を登録する手順とつまづいた箇所を紹介しました。
pass のファイルを直接編集しなかったのは、GCM が PAT のほかにユーザー名なども一緒に保存しているからです。既存の PAT を書き換えるだけなら直接編集でも動きますが、今回は新しいリポジトリの追加も混ざっていました。どちらも同じコマンドで済ませられるので、GCM に任せました。
useHttpPath = true を設定している環境では、remote URL と path の .git の有無を合わせるところがポイントです。
今回は PAT の更新よりも、応答待ちの pinentry と真っ白なウィンドウの方に時間がかかりました。コマンドがエラーもなく止まったときは、別のターミナルから ps で pinentry が残っていないか確認すると、状況を切り分けやすいです。
GUI 版の pinentry が白いウィンドウになった場合は、WSLg のセッションを疑い、作業を保存してから wsl --shutdown でリセットすると解消できました。同じ構成で PAT の更新時に詰まった方の参考になれば幸いです。
PAT の持ち方については、まだ答えが出ていません。リポジトリごとに発行すれば分離はできますが、増えていく分だけ管理も増えます。以前、組織の他の人が作成した Claude Skill をプラグインとしてインストールしたとき、Claude Code の裏側で git clone が動いて、そのリポジトリへのアクセスを許可した PAT が無くてこけたことがありました。
こういう clone は自分で URL を打つわけではないので、どのリポジトリに触ることになるか事前には読めません。1 つ 1 つ許可を足していく運用は、正直私の使い方では現実的ではないなと思っています。今回は PAT 一つに複数リポジトリを関連付けていますが、ローカルに認証情報を平文のまま置かない筋の良い方法はこの後も引き続き検討します。





