.envのトークンがGitHub CLIのログインを上書きする問題を防ぐ
今回やったこと
.env から読み込まれたGitHubトークンが、保存済みのGitHub CLIログインより優先されて失敗する問題と、その値を安全に受け渡す仕組みを整理する。対象は scripts/lib/env.py、scripts/lib/gh.py、scripts/test_gh_env_wiring.py などである。認証情報そのものは記載せず、環境変数の優先順位と子プロセスへ渡す環境の制御に焦点を当てる。
実装ステータス
共通ローダと呼び出し側の保護を実装し、横断配線検査を追加済み。横断配線検査では、同じ呼び出し方をする経路が合計25本確認された。
単体実行では成功し、別のプロセス内では失敗する
GitHub CLIは GH_TOKEN や GITHUB_TOKEN が環境にある場合、保存済みの gh auth login 情報より環境変数を優先する。問題の発端は、別用途の .env を読み込む処理がトークンも os.environ に入れ、その後に同じプロセスから gh を起動していたことだった。.env の値が対象リポジトリに必要な権限を持たなければ、保存済みログインなら成功する操作が、環境変数の値を使って失敗する。
ここで厄介なのは、gh をシェルから単独で実行する確認だけでは原因が見えないことだ。問題のプロセスは先に別の環境ローダを通っているため、子プロセスへ渡る環境が異なる。さらに呼び出し側がエラーを空配列として扱うと、認証エラーが「該当データなし」のように見える場合もあり、失敗箇所を追いにくくなる。
トークンは通常のdotenv読み込みから除く
lib/env.py は GH_TOKEN と GITHUB_TOKEN を NOT_EXPORTED として定義し、.env を os.environ へ読み込むときにこの2つを除外する。通常設定はこれまでどおり読み込みつつ、GitHubトークンだけは無条件に子プロセス環境へ広げない。環境変数を明示して使うCIやシェル側の設定まで一律に消す設計ではない。
もう一段の保護が lib/gh.py の gh_env() である。これは現在の環境をコピーし、.env に保存された値と一致するGitHubトークンだけを取り除いた環境を返す。保存済みログインがある場合にその値が優先されるようにしながら、CIや利用者が明示的に設定した別のトークンは残す。関数は元の os.environ 自体を書き換えないため、ghの子プロセスに渡す範囲で制御できる。
from lib.gh import gh_env
subprocess.run(["gh", "pr", "list"], env=gh_env())
失敗後に「別の環境でもう一度同じ書き込みコマンドを実行する」方式は採らない。たとえばIssue作成が最初の実行では成功して応答だけ失われた場合、再実行で二重作成になる可能性がある。実行前に使う環境を決めることで、書き込み処理の重複を避ける考え方だ。
配線を全体で確かめる
共通ヘルパーが存在しても、各呼び出し側が使わなければ防御にならない。test_gh_env_wiring.py はASTでPythonコードを調べ、gh を起動する経路に gh_env() が実際に使われているかを確認する。直接の subprocess.run(["gh", ...]) だけでなく、コマンドを変数へ入れて渡す形や、実行ファイルの探索を経由する形も検出対象にしている。コメントやdocstringの記述だけを実装と誤認しないよう、構文木を使う設計である。
テストには意図的に不正な呼び出し例と、正しい呼び出し例の両方がある。ヘルパーをimportしただけで使っていないケースや、env=を渡さない呼び出しを検出し、適切に環境を渡す形は誤検出しないことを確かめる。こうした正負の対照があると、監査スクリプト自体が何でも拒否する、あるいは何も検出しない状態も見つけやすい。
| 層 | 守る内容 |
|---|---|
lib.env.load_env() | dotenv由来のGitHubトークンを通常環境へ流さない |
lib.gh.gh_env() | ghの子プロセスに渡す環境を呼び出し側で制御する |
| AST配線テスト | 各Python呼び出し経路がヘルパーを実際に使うか調べる |
秘密情報の扱いは値を隠すだけでは足りない。どの設定源が優先されるか、親から子へ何が継承されるか、全ての呼び出し箇所が保護を使っているかを一続きで見る必要がある。共通ローダで入口を守り、子プロセスの環境を明示し、構文解析による横断検査で抜けを探す。この三段構成なら、個々のスクリプトがdotenvを読む方法の違いがあっても、GitHub CLIを起動する境界で同じ規則を適用できる。
更新履歴
- 2026-09-28: 初稿を作成。