Desktop: false "GitHub CLI authentication expired" toast — any `gh auth status` failure (incl. its 5s timeout) is classified as expired credentials, frequent during multi-agent workloads

Open 💬 4 comments Opened Jun 10, 2026 by zxvchaos

Preflight Checklist

  • [x] I have searched existing issues (closest matches: #7771 (closed, /install-github-app false "gh is not authenticated"), #63588, #62620, #57859 — none report this specific false-alarm toast or its classification logic)
  • [x] This is a single bug report

Environment

  • Claude desktop app 1.11847.5 (macOS)
  • macOS 26.3.1 (Tahoe), Apple Silicon, 10 cores
  • gh 2.67.0, token stored in macOS Keychain (keyring), scopes repo/workflowvalid the entire time

What's Wrong?

During heavy multi-agent sessions (many parallel subagents / workflows), the desktop app intermittently shows this toast:

GitHub CLI authentication expired. Run gh auth login to refresh pull request status.

(i18n key qG6JSl8Wq1; Japanese locale: 「GitHub CLIの認証の有効期限が切れました。gh auth login を実行してプルリクエストのステータスを更新してください。」)

The gh credentials are not expired. Immediately after the toast, gh auth status shows logged in and gh api user returns 200. Following the toast's advice (gh auth login) is wasted effort and sends users down the wrong debugging path.

Why this is a false alarm — classification logic in the app bundle

Inspecting the shipped bundles (strings in minified code, so symbol names are inferred):

  1. main.log shows the PR poller's gh calls really do time out under load:

``
2026-06-07 03:29:59 [error] [GitHubPrManager] Failed to get PR state for branch: gh pr view timed out
2026-06-07 03:45:15 [error] [GitHubPrManager] Failed to get PR state for branch: gh pr view timed out
``

  1. In app.asar, the auth check is:

``js
try {
return (await this.spawnGh(["auth","status","--hostname","github.com"],
{ cwd: e, ignoreExitCode: !0, timeoutMs: 5e3, timeoutMsg: "gh check timed out" })).code === 0
} catch { return !1 }
``

Any failure — including the 5-second spawn timeout itself — is treated as "not authenticated".

  1. In the renderer bundle, the error category "auth" maps to exactly this toast (alongside rateLimit / network / unknown categories).

So a gh invocation that is merely slow (CPU/network contention during multi-agent workloads, or a slow Keychain read) is surfaced to the user as expired credentials.

Amplifying factor: gh itself intermittently sends anonymous requests (upstream bug)

  • gh wraps all keyring operations in a hard 3-second timeout (cli/cli#7580, since v2.31.0). When the macOS Keychain read fails or times out, the token comes back empty and gh silently sends the request without an Authorization header, producing HTTP 401 — indistinguishable from expired credentials (cli/cli#13317; fix cli/cli#13318 not merged yet).
  • Live observation while preparing this report (light load, valid keyring token throughout): out of ~10 consecutive gh issue list --search ... GraphQL calls, 3 intermittently failed with HTTP 401: Requires authentication (https://api.github.com/graphql) / Try authenticating with: gh auth login; re-running the identical command seconds later succeeded. This matches the cli/cli#13317 signature (silent anonymous fallback), though gh logs nothing that would let me confirm the Keychain layer directly (that opacity is itself cli/cli#12442).

Under real multi-agent load both failure modes (slow spawn → 5s timeout; Keychain hiccup → anonymous 401) become far more likely, which is why the toast correlates with multi-agent sessions.

What I verified (ruling out actual auth problems)

  • 6 parallel clean subagent shells: all read the Keychain item (security find-generic-password -s 'gh:github.com' exit 0), gh auth status OK, 30/30 gh api user calls succeeded, no GH_TOKEN/GITHUB_TOKEN env vars involved.
  • Stress tests: 32 concurrent gh auth token (pure Keychain reads) on an idle system, then 24 concurrent reads with all 10 CPU cores saturated — 0 failures, slowest 0.74 s. The failure needs real-world load patterns (process storms + network saturation), not just CPU pressure.

Suggested fixes

  1. Classify by evidence, not by failure: only emit the "auth" category when gh's output actually indicates an auth problem (e.g. stderr matching "not logged in"). Timeouts and spawn failures should map to network/unknown ("PR status may be out of date"), which already have honest copy.
  2. Debounce/retry the auth check before toasting — a single 5-second-timeout sample taken while the machine is under heavy agent load is weak evidence for "credentials expired".
  3. Optionally cache the token once at app start (e.g. inject GH_TOKEN into spawned gh processes) so the PR poller stops hitting the Keychain on every poll — this also fully sidesteps upstream cli/cli#13317.

Related

  • #7771 (closed) — /install-github-app falsely claims gh is not authenticated
  • #63588 — PR status chip greyed out despite gh reporting correct state
  • #62620 — desktop CI monitoring panel misleading error
  • #57859 — PR/CI panel can't find gh due to PATH (different root cause, same surface)
  • Upstream: cli/cli#13317, cli/cli#7580, cli/cli#13318, cli/cli#12442

---

Update (2026-06-11) — consolidated root cause after two independent reproductions

Thanks @josephjguerra (macOS 26.5.1, gh 2.86.0) and @dcarter00 (Windows 11, GH_TOKEN set — credential store fully bypassed) for the reproductions in the comments. The refined picture:

The desktop-side classifier is the core bug, and it is cross-platform — not a macOS Keychain quirk. At least four independent feeder paths all land in the same "auth" → expired-credentials toast:

  1. Keyring/Keychain read timeout → gh silently sends an anonymous request → 401 (macOS; upstream cli/cli#13317)
  2. Transient GitHub API/network 401s — observed even with GH_TOKEN set, i.e. with the credential store entirely out of the loop (Windows, corporate VPN)
  3. Main-process event-loop stalls under multi-agent load ([event-loop-stall] main process blocked for 2861ms observed) stacked on gh's ~1.2–2s baseline gh auth status latency → blows the 5s timeoutMs
  4. Spawning gh with a deleted cwd (e.g. a removed worktree) → instant spawn failure → catch { return false } (deterministic repro on Windows: "The directory name is invalid")

Additional symptoms reported alongside, worth fixing together:

  • The failure can be completely silent: the token-fetch path returns null inside a catch and logs only at debug level, so the toast can fire with no corresponding main.log entry.
  • The toast persists after the next successful poll and reappears across full relaunches until manually dismissed.

Consolidated suggested fixes (superseding the list above):

  1. Classify by evidence, not by failure: show the expired-credentials toast only on a genuine auth signal (e.g. gh auth status exiting non-zero with auth-related stderr such as "not logged in"). Map timeouts → network; spawn failures / ENOENT / invalid cwd → session-state; a lone transient 401 → retry before classifying.
  2. Debounce/retry before toasting.
  3. Log auth-path failures at info/warn instead of swallowing them at debug level, so the toast is diagnosable from main.log.
  4. Auto-dismiss the toast once a subsequent poll succeeds.

🤖 Generated with Claude Code

View original on GitHub ↗

This issue has 4 comments on GitHub. Read the full discussion on GitHub ↗