[BUG] Desktop app shows 'GitHub CLI authentication expired' banner while gh credentials are valid (Keychain storage)
Environment
- Claude Code version: 2.1.143 (desktop app, macOS)
- Platform: macOS 15.6.1 (Sequoia)
- gh version: 2.79.0 (2025-09-08)
- gh token storage: macOS Keychain (
keyring)
Bug description
The desktop app repeatedly shows this banner:
⚠️ GitHub CLI authentication expired. Run gh auth login to refresh pull request status.
But the GitHub CLI credentials are valid at the moment the banner appears. Verified in the same session, seconds apart:
$ gh auth status
github.com
✓ Logged in to github.com account earlyspark (keyring)
- Active account: true
- Git operations protocol: https
- Token: gho_************************************
- Token scopes: 'admin:public_key', 'gist', 'read:org', 'repo', 'workflow'
$ gh api user --jq .login
earlyspark
The banner reappeared after being dismissed, again with gh auth status and a live gh api call succeeding at the same time.
Expected behavior
No "authentication expired" banner when gh credentials are valid and working.
Suspected cause
The token is stored in the macOS Keychain. If the desktop app checks PR status by invoking gh from a background/non-interactive context, Keychain reads can fail there even though they succeed in an interactive shell — and the failure is then misreported as an expired token. This would make the banner's suggested fix (gh auth login) a no-op, since the token itself is fine.
Possibly related:
- #7771 —
/install-github-appclaimsghis not authenticated when it is - #56000 — GitHub repository access check fails despite valid
ghcredentials - #44089 / #44028 — macOS Keychain inaccessible outside a GUI shell session
Steps to reproduce
- macOS, Claude Code desktop app,
ghauthenticated with token in Keychain (gh auth login, default secure storage). - Use the app normally in a git repo with a GitHub remote.
- Banner appears claiming
ghauth expired;gh auth statusandgh api usersucceed at the same moment.
Impact
Cosmetic but recurring — the banner re-appears after dismissal and prompts users toward an unnecessary re-login (or worse, toward --insecure-storage to silence it).
4 Comments
Same symptom on macOS 26.5, desktop app 1.11847.5 (embedded claude-code runtime 2.1.170), gh 2.86.0 (Homebrew, keyring storage).
Diagnostics collected while the banner was showing:
gh auth status,gh api user, andgh pr statusall succeed, including under a minimal launchd-like env (env -i HOME=... PATH=/usr/bin:/bin:/usr/sbin:/sbin), so keychain access from a bare environment is fine.main.loghas zero gh/auth-related[error]entries for the whole day.LocalSessions.getPrCheckspolls run every 30s and log no failures.ps lstart) and reappears without any corresponding error entry in the logs.So the "auth" classification fires (or persists) without any logged failing gh call. This is consistent with the theory in #67055: a transient
gh auth statusfailure or its 5s timeout at startup (this machine launches a dozen MCP servers at once) gets classified as expired credentials, and once the degraded flag is set there appears to be no path that resets it when subsequent PR-check polls succeed.Workarounds tried: re-running
gh auth login(device flow, fresh token verified working) does not clear the toast; dismissing it works only until the next app launch.HINT: It may happen that different gh cli are active and your prompt uses another version than Claude. In this case you may authenticate but Claude will not recognize it. Ask Claude to find all installations of gh and use the same as your are using. This may remove the issue.
I'm experiencing the same behavior on Windows 11 with claude desktop. I'm afraid I cannot give you the claude version as the main menu is not accessible using a screen reader (I'm blind and use NVDA on Windows 11). I did restart claude, and my laptop as well, on July 19th, to update claude. I should be on a recent version.
I do not know where the gh cli authentication expired information persists graphically on the desktop app. It presents itself to me though as I ctrl+uparrow to read claude's output. From the input multiline text element, I go into browse mode in NVDA to arrow around the GUI. The first output I hear is regarding creating a pull request and that my gh cli authentication has expired. I connected claude to my GH account late afternoon (PDT) July 19th and had it create a repo and push a new project I was working on to GH. That all worked fine, including my running gh auth login to give claude permission initially. The create PR and gh cli authentication has expired text has been there ever since, even after updating claude and restarting my laptop.
I have confirmed with claude itself that it has full access to my gh account and nothing has expired. It suggested opening a ticket. I found this one and am suspecting this is not a problem unique to MacOS.
Root cause, from disassembling the shipped bundle. This is not a Keychain-access problem — that theory can't explain @dcarter00's Windows reproduction with the keyring bypassed (#67055). The real cause is simpler and fully deterministic: the check blanks the very environment variables that hold the active credential.
The code
~/.local/share/claude/versions/2.1.214, token resolver:and the label that surfaces:
Resolution order is: (1) read
GH_TOKEN/GITHUB_TOKENfrom env if the host matches; else (2) spawngh auth token --hostname <host>with those vars blanked; non-zero exit →{kind:"no-token"}→ classifiedneeds-auth.Why that misfires
If step 1's host match fails for any reason, step 2 runs a subprocess that has been deliberately stripped of the working credential and must fall back to the keyring. So the check can only ever succeed via a store the user may not be using. Three independent ways to land on a false
needs-auth:GH_TOKEN; the keyring entry isActive account: false. The check hides the good token from itself, then reports the fallback's failure as expired.preserveOutputOnError: falsediscards stderr and the verdict is exit-code-only, so "gh binary missing", "keyring locked", "timed out", and "genuinely unauthenticated" all collapse into the sameneeds-auth.What I could and could not reproduce
Being precise about this, because it narrows the search:
On my machine that second command exits 0 — a keyring token exists, so the fallback answers and the blanking alone does not trigger the banner. So I am not claiming env-blanking is the deterministic trigger here.
What it does establish: the check cannot see the credential the user is actually authenticating with, and depends entirely on a secondary store answering. Where that store is absent, locked, slow, or host-mismatched, the result is a false
needs-auth— and the banner then tells the user to rungh auth login, which writes to the keyring, i.e. the storeghdoes not read when the env var is set. That is why re-authenticating cannot fix it: ~20 re-auths and rotations on this machine changed nothing.That also fits @dcarter00's Windows/keyring-bypassed reproduction, which the Keychain-access theory alone doesn't explain.
Suggested fixes
GH_TOKEN/GITHUB_TOKENis set and valid for the host, that is the answer.ghmissing / timeout / keyring-locked / actually-unauthenticated are four different states; only the last warrants "authentication expired". Keep stderr (preserveOutputOnError: true) to tell them apart.Workaround
/config→ disable "Show PR status footer" (prStatusFooterEnabled: falseinsettings.json). Cosmetic feature; nothing operational is lost.Given #67055 was auto-closed as
NOT_PLANNEDfor inactivity after three cross-platform reproductions, this one seems the right place to consolidate. Happy to test a patch.<sub>Env: Claude Code 2.1.214, macOS, gh 2.96.0, token in env var (
GH_TOKEN),gh auth statusexit 0 throughout.</sub>