diff --git a/.claude/skills/auth-token-safety/SKILL.md b/.claude/skills/auth-token-safety/SKILL.md index 4732b64073..1352741148 100644 --- a/.claude/skills/auth-token-safety/SKILL.md +++ b/.claude/skills/auth-token-safety/SKILL.md @@ -70,9 +70,14 @@ Do NOT re-read from Keychain on every recovery cycle. - ❌ Auth polling loop calling `ResolveGitHubTokenForServer()` on every auth detection - ✅ Only `ReauthenticateAsync` (explicit user action) re-reads Keychain - ✅ `TryRecoverPersistentServerAsync` and polling use the cached `_resolvedGitHubToken` +- ✅ `CheckAuthStatusAsync` sets `_resolvedGitHubToken ??= string.Empty` on auth success + so later transient failures don't trigger the lazy Keychain path **Why:** Each Keychain read = another password dialog. The polling loop runs every 10s. -If it re-reads Keychain, users get prompted every 10 seconds. +If it re-reads Keychain, users get prompted every 10 seconds. The sentinel (`""`) on +auth success prevents the lazy path from firing when the server can self-authenticate — +without it, `_resolvedGitHubToken` stays null after startup (no env var set), and any +transient auth failure triggers 3 Keychain reads (3 service names × 3s timeout = 3 dialogs). ### INV-A3: Never clear `_resolvedGitHubToken` on automatic recovery diff --git a/PolyPilot/Services/CopilotService.Utilities.cs b/PolyPilot/Services/CopilotService.Utilities.cs index 826b80866d..446ee4d045 100644 --- a/PolyPilot/Services/CopilotService.Utilities.cs +++ b/PolyPilot/Services/CopilotService.Utilities.cs @@ -865,6 +865,11 @@ private async Task CheckAuthStatusAsync() if (status.IsAuthenticated) { StopAuthPolling(); + // Mark that the server can self-authenticate — no Keychain read needed. + // Without this, _resolvedGitHubToken stays null after startup (no env var), + // and any later transient auth failure triggers the lazy Keychain path + // (3 service names × 3s timeout each = 3 macOS password dialogs). + _resolvedGitHubToken ??= string.Empty; InvokeOnUI(() => { AuthNotice = null;