-
Notifications
You must be signed in to change notification settings - Fork 3.3k
[Bug] macOS Keychain infinite prompt loop - 'Allow' vs 'Always Allow' causes corrupted state #6595
Description
Describe the bug
On macOS, users experience an infinite keychain password prompt loop when using Goose Desktop with GitHub Copilot provider. The prompts continue indefinitely until the user clicks "Always Allow" — clicking just "Allow" causes prompts to recur on every keychain operation.
Critical finding: If user clicks "Allow" first (instead of "Always Allow"), they enter a corrupted state that requires two subsequent "Always Allow" clicks to recover.
Root Cause Analysis
We traced through the code and confirmed via extensive testing:
Why the loop happens
-
CLI and Desktop use different binaries with different code signatures:
- CLI:
~/.local/bin/goose(cdhash-based ACL, unsigned) - Desktop:
/Applications/Goose.app/Contents/Resources/bin/goosed(Team ID-based ACL, signed by Block Inc.)
- CLI:
-
When CLI authenticates first, the keychain entry's ACL only includes the CLI's cdhash. Desktop's
goosedbinary has a different signature and isn't authorized. -
Each keychain operation is a separate prompt when user clicks "Allow":
get_secret()= 1 READ operationset_secret()= 1 READ + 1 WRITE operation (reads existing secrets, then writes updated)
-
Retry loops compound the problem:
with_retry (3 retries) — providers/retry.rs └─ post() └─ get_api_info() (3 internal retries) └─ refresh_api_info() └─ config.get_secret() ← KEYCHAIN READ
Test Results
| First Click | Subsequent Clicks | Result |
|---|---|---|
| Always Allow | — | ✅ Works immediately |
| Allow | Allow (20+ times) | ❌ Infinite loop |
| Allow | Always Allow | ❌ Fails first time |
| Allow | Always Allow (2nd) | ✅ Works |
The "two Always Allow clicks needed" behavior occurs because:
- First "Always Allow" authorizes the read operation
- Second "Always Allow" authorizes the write operation (since
set_secret()does read+write)
ACL Evidence
After clicking "Always Allow", both binaries are properly authorized:
applications (2):
0: /Applications/Goose.app/Contents/Resources/bin/goosed (OK)
requirement: identifier goosed and anchor apple generic and certificate leaf[subject.OU] = EYF346PHUG
1: /Users/jacobhales/.local/bin/goose (OK)
requirement: cdhash H"3443534d499f2ae1d2cc22fe2571c1f66a69085a"
Note: Desktop uses Team ID-based requirement (stable across updates), while CLI uses cdhash (breaks on updates).
Proposed Fixes
Fix 1: Cache OAuth token in memory (eliminates retry prompts)
// In GithubCopilotProvider struct, add:
oauth_token: tokio::sync::OnceCell<String>,
// In refresh_api_info(), cache after first read:
let token = self.oauth_token.get_or_try_init(|| async {
config.get_secret::<String>("GITHUB_COPILOT_TOKEN")
}).await?;Fix 2: Pre-keychain UX guidance
Before triggering the first keychain access, show an in-app dialog:
"macOS will ask for keychain access. Click 'Always Allow' to avoid repeated prompts."
Fix 3: Document the CLI→Desktop workflow
If users authenticate via CLI first, they need to click "Always Allow" when Desktop prompts (since it's a different binary).
To Reproduce
- Clear keychain:
security delete-generic-password -s goose - Clear cache:
rm -rf ~/.config/goose/githubcopilot/ - Run
goose configure→ select GitHub Copilot → complete OAuth - Launch Goose Desktop
- When keychain prompt appears, click "Allow" (not "Always Allow")
- Observe: prompts continue indefinitely (20+ tested)
To escape: Click "Always Allow" twice (once for read, once for write)
Expected Behavior
- Single keychain prompt on first access
- Clear guidance to click "Always Allow"
- No prompts after authorization (until app update changes binary signature)
Environment
- OS & Arch: macOS 15.2 (Sequoia) arm64
- Interface: Desktop + CLI
- Version: v1.20.1 (Homebrew)
- Provider & Model: GitHub Copilot (claude-sonnet-4.5)
Additional Context
- Related closed issues: Goose Desktop stuck in loop asking for Keychain access #1734, Goose requesting keychain password repeatedly #4018, Mac desktop app repeatedly prompts for keychain access #1238 (all closed with "click Always Allow" but issue persists for users)
- Non-technical users don't understand "Allow" vs "Always Allow" difference
- The Desktop binary uses Team ID-based ACL (survives updates), but CLI uses cdhash (breaks on updates)
References
- Keychain access:
config/base.rs:542-560(all_secrets()) - Set secret (read+write):
config/base.rs:765-790(set_secret()) - Retry loop:
providers/githubcopilot.rs:200-221(get_api_info())