Skip to content

[Bug] macOS Keychain infinite prompt loop - 'Allow' vs 'Always Allow' causes corrupted state #6595

@jahales

Description

@jahales

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

  1. 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.)
  2. When CLI authenticates first, the keychain entry's ACL only includes the CLI's cdhash. Desktop's goosed binary has a different signature and isn't authorized.

  3. Each keychain operation is a separate prompt when user clicks "Allow":

    • get_secret() = 1 READ operation
    • set_secret() = 1 READ + 1 WRITE operation (reads existing secrets, then writes updated)
  4. 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:

  1. First "Always Allow" authorizes the read operation
  2. 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

  1. Clear keychain: security delete-generic-password -s goose
  2. Clear cache: rm -rf ~/.config/goose/githubcopilot/
  3. Run goose configure → select GitHub Copilot → complete OAuth
  4. Launch Goose Desktop
  5. When keychain prompt appears, click "Allow" (not "Always Allow")
  6. 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

References

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions