- GitHubで公開されているコードを自分用にカスタマイズした、Codexの使用率を表示するツールが「更新失敗」になり、同じ数字を出したまま止まっていました。原因は、裏で使っていたCodex CLIのログインが切れていたことでした。
- アプリのログには「タイムアウト」とされていましたが、Codex側のログを開くと、
401 refresh_token_invalidatedと記録されていました。 - Codexは
~/.codex/auth.jsonに、有効期限の違う3種類のトークンを入れています。期限の短いアクセストークンは自動で取り直せますが、取り直すためのリフレッシュトークンが無効になると、ログインし直すしかありません。 codex loginでブラウザからサインインし直し、スマホで本人確認をしたら、その場で表示が戻りました。
1. Codex使用率の取得でエラー

Codexの使用率を、メニューバーのアプリで見ています。
5時間の枠と週間の枠が、あと何パーセント残っているかを常に出してくれるものです。
ある日ポップオーバーを開くと、下の「更新」ボタンが「更新失敗」になっていました。

週間は98%使用、残り2%。
最終更新は9月11日の8:38で、これを見ていたのは9月13日の昼です。
数字は出ているので、一瞬、使いすぎただけかと思いました。
でも、2日前の数字をそのまま出していたわけです。
2. 使用率アプリは、Codex CLIのログインを借りていた
2.1. メニューバーのアプリは、自分では使用率を取りに行かない
使っているのは ai-usage-menubar という、CodexとClaude Codeの使用率を並べて表示するmacOSのアプリです1。
手元では、GitHubで公開されているコードをクローンして、自分用にカスタマイズして使っています。
「更新失敗」の理由を探すには、このアプリがどこから数字を持ってきているのかを知る必要があります。
このアプリではOpenAIのサーバーに直接つなぐ設計にはなっていません。
Codex CLIを app-server --stdio という引数で起動して、標準入力にJSONを3行書き込んでいます。
{"id":1,"method":"initialize","params":{"clientInfo":{"name":"ai-usage-menubar","version":"0.1.8"}}}
{"method":"initialized"}
{"id":2,"method":"account/rateLimits/read"}Code language: JSON / JSON with Comments (json)
ターミナルで codex app-server --stdio を起動して、この3行を手で打ち込んでみると、id が2の応答として利用枠が返ってきます。
5時間枠が primary、週間枠が secondary で、使用率の usedPercent とリセット時刻の resetsAt が入っています。
ためてあるリセットの残り回数も rateLimitResetCredits に一緒に入っていました。

これはJSON-RPC(JSON Remote Procedure Call:JSONで書く遠隔手続き呼び出し)という形式の要求です。
app-serverは、Codexのデスクトップアプリやエディタの拡張機能が、Codex本体とやりとりするための窓口です2。
接続を済ませてから account/rateLimits/read を頼むと、5時間枠と週間枠の使用率とリセット時刻が返ってきます3。
つまり、アプリはCodex CLIに「使用率を聞いてきて」と頼んでいるだけ。
OpenAIへの問い合わせも、そのときの認証も、全部Codex CLIがやっています。
アプリ自身は認証情報を持っていません。
そのため、Codex CLIのログインが切れれば、アプリも一緒に止まります。
2.2. Codexに話しかけたら、赤い字で理由が出た
そこでCodex CLIを直接起動して、/status を打ってみました。
アカウントもプランもふつうに表示されますが、いちばん下の Limits の欄は「refresh requested; run /status again shortly.」。
試しに「こんにちは。」と送ってみると、赤い字でエラーが返ってきました。

Your access token could not be refreshed because your refresh token was revoked. Please log out and sign in again.
アクセストークンを更新できなかった、リフレッシュトークンが取り消されているから、ログアウトしてサインインし直してください。/status の画面ではなく、何か指示を出してみて、ようやく理由が出てきたわけです。
3. codex loginでサインインし直すと、その場で戻った

直し方は、エラーの文面どおりです。
ログアウトして、サインインし直す。
コマンドで書くと、この2つです。
codex logout
codex login
codex login を実行すると、ブラウザでChatGPTのサインイン画面が開きます。
裏では、Codexが手元のMacで小さなWebサーバーを立てて、サインインが終わった知らせをブラウザから受け取る仕組みになっています4。
サインインを進めると、iPhoneのChatGPTアプリに確認の画面が出ました。
「ChatGPTにサインインしようとしているのはご本人ですか?」。
端末は「Mac OS X」、時刻は12:34と出ています。

ここで承認すると、ブラウザに「Signed in to Codex」と出ました5。

Codex CLIでもう一度 /status を打つと、今度は利用枠が表示されます。
5時間枠も週間枠も、100% left。

auth.json の更新時刻も12:35に変わり、last_refresh も新しくなっていました。
メニューバーのアプリは、1分後の12:36に更新に成功しています。
ポップオーバーの週間残量のグラフには、日曜の位置に100%の棒が立ちました。

4. アクセストークンとリフレッシュトークンは、役目も期限も違う

4.1. auth.jsonには期限の違う3つのトークンが入っている
Codexは、ChatGPTのアカウントでサインインした情報を ~/.codex/auth.json に保存しています6。
中身を人に見せるわけにはいかないので、キーの名前と、トークンに書かれた発行時刻と有効期限だけを取り出してみました。
python3 - <<'EOF'
import json, base64, datetime, os
d = json.load(open(os.path.expanduser('~/.codex/auth.json')))
for k in ('id_token', 'access_token'):
p = d['tokens'][k].split('.')[1]
p += '=' * (-len(p) % 4)
c = json.loads(base64.urlsafe_b64decode(p))
print(k, (c['exp'] - c['iat']) / 3600, 'hours')
EOFCode language: JavaScript (javascript)
tokens の下には id_token、access_token、refresh_token、account_id が並んでいて、その横に last_refresh という日時があります。id_token と access_token はJWT(JSON Web Token:ジェイソン・ウェブ・トークン)という形式で、点で区切った真ん中の部分に、発行時刻の iat と期限の exp が書かれています7。
計算すると、id_token は1時間、access_token は240時間、つまり10日でした。
アクセストークンは、APIに「この人のリクエストです」と示すための通行証です。
期限を短くしておけば、漏れたときの被害も短く済みます。
その代わり、期限が来るたびに取り直さなければなりません。
そのたびにブラウザでサインインさせるのは面倒なので、取り直し専用の鍵を別に持たせておく。
これがリフレッシュトークンです8。
Codexは、使っているあいだにこの取り直しを自動でやっています9。
ふだんログインのことを意識しないで済んでいるのは、このためです。
4.2. リフレッシュトークンが無効にされると、自動では戻れない
今回切れていたのは、アクセストークンではなく、取り直し用のリフレッシュトークンのほうでした。
そのため、最初と同じくブラウザでサインインし直す必要があります。
なぜリフレッシュトークンが無効にされたのかは、はっきりしません10。
仕組みの上で考えられるのは、リフレッシュトークンの「ローテーション」です。
ローテーションは、リフレッシュトークンを一度使ったら、新しいものと交換して古いほうを使えなくするやり方です。
盗まれたトークンが後から使われると、サーバーは「使用済みのトークンがまた来た」と気づけます。
そのときは、一連のトークンをまとめて無効にするのが安全とされています11。
Codexの公式ドキュメントにも、それらしい注意書きがあります。last_refresh がおよそ8日より古くなると更新をかけること、同じ auth.json を複数のマシンや同時に動くジョブで共有しないこと、別のマシンやジョブが先にトークンをローテーションすると更新に失敗することがある、と書かれていました12。
手元のMacでは、ターミナルのCodex CLIと、メニューバーのアプリが10分ごとに起動するapp-serverが、同じ auth.json を読んでいます。
どちらかが更新をかけた瞬間にもう片方が古いトークンで更新しに行けば、ぶつかる可能性はある。
ただ、後で開くCodexのログには9月5日からの記録が残っていて、失敗が始まるまでのあいだに、トークンの更新を試みた記録は1件もありませんでした。
奪い合いが起きていた形跡はなく、今回の原因だったとは言えません。
5. ログを突き合わせると、2日間失敗し続けていた
5.1. アプリのログには「タイムアウト」としか残らない
アプリの設定画面には「詳細」タブがあって、更新の成否を記録したデバッグログを見られるようにしています。
保存先は ~/Library/Logs/ai-usage-monitor/debug.log です。

Codexの行だけを拾って、成功と失敗が何回続いたかを数えてみました。
grep "service=codex" ~/Library/Logs/ai-usage-monitor/debug.log \
| grep -E "failed|succeeded" | awk '{print $1, $2, $4}' | uniq -c -f2Code language: JavaScript (javascript)
ログの時刻はUTCなので、日本時間に直して書きます。
9月11日の13:23から失敗が始まり、9月13日の12:36に成功するまで、250回連続で失敗していました。
2日近く、10分おきに取りに行っては失敗していたわけです。
気になったのは、失敗の中身が全部 error_domain=UsageLiveFetch error_code=2 だったことです。
アプリのソースでは、コード2は「Codex app-serverがタイムアウトしました」。
所要時間はどれも10〜11秒で、アプリが決めている待ち時間の10秒で打ち切られていました。
認証が切れているのに、なぜタイムアウトになるのか。
アプリは、app-serverが返してくる出力を読みながら、利用枠のデータが来るのを待っています。
認証エラーが返ってきても、それは利用枠のデータではないので、アプリは「まだ来ない」と待ち続けていました13。
app-serverのほうは次の要求を待って起動したままなので、出力も終わらない。
結局10秒経って打ち切られ、「タイムアウト」として記録されたようです。
5.2. Codexのログには401が残っていた
では、Codex側には何が残っているのか。
Codexは ~/.codex/ の下に、SQLiteのデータベースでログを持っています。logs_2.sqlite の logs テーブルに、時刻の ts、重さの level、本文の feedback_log_body などの列がありました。
sqlite3 ~/.codex/logs_2.sqlite \
"select datetime(ts,'unixepoch','localtime'), level, substr(feedback_log_body,1,300)
from logs
where feedback_log_body like '%refresh_token_invalidated%'
order by ts limit 3;"Code language: PHP (php)
最初に出てくるのは、日本時間で9月11日の14:30です。
サーバーからの応答が、そのまま残っていました。
auth: Failed to refresh token: 401 Unauthorized: {
"error": {
"message": "Your refresh token has been invalidated. Please try signing in again.",
"type": "invalid_request_error",
"param": null,
"code": "refresh_token_invalidated"
}
}Code language: JavaScript (javascript)
同じ時刻に、使用率を取りに行った要求も 401 Unauthorized で弾かれています。
こちらのコードは token_expired で、「認証トークンの期限が切れています」。
アクセストークンが期限切れになり、取り直そうとしたら、リフレッシュトークンのほうが無効にされていた、という順番です。
記録を見ると、呼び出し元の名前は ai-usage-menubar でした。
「refresh token was revoked」という行は、9月13日の再ログインまでに1008件ありました。
アプリが10分ごとにapp-serverを起こすたびに、同じ失敗が積み上がっていたことになります。
なお、アプリの失敗が始まった13:23から14:30までのあいだは、Codexのログに行がありませんでした。
5.3. 使用率のリセットタイミング?
ひとつ、わからないことが残っています。
再ログインの前、週間は98%使用で、リセットは9月15日の予定でした。
それが、戻ってきたときには0%使用で、リセットは9月20日になっていました。
ためてあるリセットを使うと週間の枠が戻り、リセット日も変わるそうですが、画面には「3回分のリセットが使えます」と出たままで、使った覚えもありません14。
ログインが切れていた2日のあいだに何かあったのか、再ログインが関係しているのかは、確かめられていません。
ログインの状態は、ふだん目に見えません。
見えないまま、別のアプリがそれを借りて動いている。
エラーの本文は、借りた側に届くまでのどこかで「タイムアウト」に変わっていました。
表示が止まったら、まずは借り元のログを開けてみる。
今回はそれで、2日分の沈黙の理由が読めました。
- もとは有志が公開しているオープンソースのアプリで、Codex版はCodex CLIのapp-serverから
/statusと同じアカウントの利用枠を取得し、~/.codex/sessionsの記録を補助に使う、とREADMEに書かれています。 – akh1r0ck/ai-usage-menubar - app-serverは標準入出力の上でJSON-RPC 2.0を話す常駐プロセスで、
initializeの要求とinitializedの通知で握手を済ませてから要求を受け付けます。デスクトップアプリ、VS Codeの拡張機能、CLI自身もこの仕組みの上で動いています。 – The Codex App-Server: Building Custom Integrations with the JSON-RPC Protocol - 応答の
primaryがおおむね5時間枠(300分)、secondaryが週間枠(10080分)で、それぞれにusedPercent、windowDurationMins、resetsAtが入ります。 – How to check Codex usage limits with App Server (account/rateLimits/read) - ブラウザでのサインインでは、Codexがローカルの1455番ポートでコールバックを受けるHTTPサーバーを立て、PKCEの検証用の値を作ってから
auth.openai.comの認可画面を開き、戻ってきた認可コードをアクセストークンとリフレッシュトークンに交換します。 – Codex CLI Authentication: OAuth, Device Code, API Keys, and CI/CD Credential Management - OpenAIのヘルプでは、新しい端末や見慣れない端末からのログインなどで、モバイルアプリでの承認やメールで届くコードの入力といった追加の確認を求めることがある、と説明されています。 – Why Am I Being Asked to Verify My Login?
- 公式の説明では、ログイン情報は
~/.codex/auth.jsonの平文ファイルか、OSの資格情報ストアに保存されます。ファイルで保存する場合は、アクセストークンが入っているのでパスワードと同じように扱うよう注意書きがあります。 – Authentication expはその時刻以降はトークンを受け入れてはならない期限、iatは発行された時刻を表す、とJWTの仕様で決められています。 – RFC 7519: JSON Web Token (JWT)- OAuth 2.0の仕様では、リフレッシュトークンは「アクセストークンを得るための資格情報」で、今のアクセストークンが無効になったり期限が切れたりしたときに、新しいアクセストークンを得るために使うと定義されています。 – RFC 6749: The OAuth 2.0 Authorization Framework
- 公式ドキュメントには、ChatGPTでサインインしたセッションは使用中に期限切れの前に自動で更新されるので、通常はブラウザでのログインをし直さなくても続けられる、とあります。 – Authentication
- Codexのデスクトップアプリで、使っている最中に突然この「refresh token was revoked」の状態になるという報告がGitHubに上がっていますが、原因についての開発側の説明は見つけられませんでした。今回の失効の原因も確認できていません。 – Codex Desktop gets stuck with refresh-token-revoked auth error during active session
- OAuth 2.0のセキュリティのベストプラクティスでは、クライアント認証を持たない公開クライアントのリフレッシュトークンは、送信者に紐づけるか、ローテーションを使わなければならないとしています。ローテーションでは、使うたびに新しいトークンを発行し、古いものを無効にします。 – RFC 9700: Best Current Practice for OAuth 2.0 Security
- CI/CDでCodexのアカウント認証を維持するための手引きに、更新が止まる場面として「リフレッシュトークンが取り消されたか期限切れになった」「同時に動く別のジョブが先にトークンをローテーションした」が挙げられています。直し方は、信頼できるマシンで
codex loginをし直してauth.jsonを置き換えることです。 – Maintain Codex account auth in CI/CD (advanced) - JSON-RPC 2.0では、応答は成功なら
result、失敗ならerrorのどちらか一方を持ちます。エラーの応答を受け取っても、resultだけを探している読み手には「まだ応答が来ていない」ように見えます。 – JSON-RPC 2.0 Specification - 公式ヘルプでは、ためてあるリセット(banked reset)を使うと5時間枠と週間枠が戻り、週間のリセット日が変わると説明されています。全体に適用されたリセットとして書かれているのは9月7日のもので、9月11日から13日のあいだに週間枠が戻った理由は確認できていません。 – How banked Codex resets work