[BUG] Desktop app shows 'GitHub CLI authentication expired' banner while gh credentials are valid (Keychain storage)

Status Open
Reported on v2.1.143
Maintainer reply None cached
Activity 4 comments · opened Jun 10, 2026

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-app claims gh is not authenticated when it is
  • #56000 — GitHub repository access check fails despite valid gh credentials
  • #44089 / #44028 — macOS Keychain inaccessible outside a GUI shell session

Steps to reproduce

  1. macOS, Claude Code desktop app, gh authenticated with token in Keychain (gh auth login, default secure storage).
  2. Use the app normally in a git repo with a GitHub remote.
  3. Banner appears claiming gh auth expired; gh auth status and gh api user succeed 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).

View original on GitHub ↗

4 Comments

stomashevsky · 2 months ago

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, and gh pr status all 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.log has zero gh/auth-related [error] entries for the whole day. LocalSessions.getPrChecks polls run every 30s and log no failures.
  • The toast survives a full app restart (main process re-spawn confirmed via 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 status failure 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.

jthDEV · 2 months ago

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.

joeldodson · 1 month ago

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.

mishaal-cloud · 1 month ago

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:

["auth","token","--hostname",e], {
  timeout: 5000,
  preserveOutputOnError: false,
  env: { ...process.env, GH_TOKEN:"", GITHUB_TOKEN:"", GH_ENTERPRISE_TOKEN:"", GITHUB_… }
}

and the label that surfaces:

case "needs-auth": return "gh auth login for PR status"

Resolution order is: (1) read GH_TOKEN/GITHUB_TOKEN from env if the host matches; else (2) spawn gh auth token --hostname <host> with those vars blanked; non-zero exit → {kind:"no-token"} → classified needs-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:

  • Env-var auth (my case). My active account is GH_TOKEN; the keyring entry is Active account: false. The check hides the good token from itself, then reports the fallback's failure as expired.
  • 5s timeout under parallel-subagent load — matches the multi-agent trigger in #67055.
  • Non-interactive keyring reads — the original theory here, real but only one of several feeders.

preserveOutputOnError: false discards stderr and the verdict is exit-code-only, so "gh binary missing", "keyring locked", "timed out", and "genuinely unauthenticated" all collapse into the same needs-auth.

What I could and could not reproduce

Being precise about this, because it narrows the search:

export GH_TOKEN=$(gh auth token)
gh auth status; echo "exit=$?"     # → exit=0, valid

# exactly what the footer runs:
env GH_TOKEN= GITHUB_TOKEN= GH_ENTERPRISE_TOKEN= gh auth token --hostname github.com
echo "exit=$?"

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 run gh auth login, which writes to the keyring, i.e. the store gh does 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

  1. Don't blank the token env vars. If GH_TOKEN/GITHUB_TOKEN is set and valid for the host, that is the answer.
  2. Distinguish the failure modes. gh missing / timeout / keyring-locked / actually-unauthenticated are four different states; only the last warrants "authentication expired". Keep stderr (preserveOutputOnError: true) to tell them apart.
  3. Don't assert expiry from an exit code alone — the current message states a fact it hasn't established, which is what sends users into credential rotation that cannot work.

Workaround

/config → disable "Show PR status footer" (prStatusFooterEnabled: false in settings.json). Cosmetic feature; nothing operational is lost.

Given #67055 was auto-closed as NOT_PLANNED for 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 status exit 0 throughout.</sub>