Skip to content

Prevent Keychain prompts during automatic refreshes; show an actionable error card - #24

Merged
mstallone merged 2 commits into
mainfrom
fix-claude-keychain-bug
Jul 30, 2026
Merged

Prevent Keychain prompts during automatic refreshes; show an actionable error card#24
mstallone merged 2 commits into
mainfrom
fix-claude-keychain-bug

Conversation

@mstallone

Copy link
Copy Markdown
Owner

TL;DR

Automatic refreshes can no longer trigger macOS Keychain password dialogs — a protected item fails fast into the friendly "Permission Required" state — and a provider with nothing to show now gets an actionable error card instead of a wall of empty "No data" rows.

What was happening

  • Launch and background refreshes read classic login-keychain items (Claude Code credentials, Claude Desktop / browser Safe Storage keys) guarded only by LAContext.interactionNotAllowed. The classic ACL confirmation dialog ignores that flag, so macOS could unexpectedly show "Runway wants to use your confidential information…" during an automatic refresh — repeatedly on first run — and the refresh sat blocked behind the dialog (~15 minutes in one report).
  • A provider stuck in that state rendered empty metric bars with only a header warning triangle: no explanation, no way forward.

What this changes

  • Adds a process-wide KeychainUISuppression gate built on SecKeychainSetUserInteractionAllowed — verified empirically as the only switch the classic keychain ACL path honors. Every non-interactive Security.framework read/probe/update runs suppressed and returns promptly (protected item → errSecAuthFailed → Permission Required, no fallback to a possibly stale credentials file); interactive reads from a manual Refresh hold the gate for their whole duration so a racing background read can't disable UI beneath an open approval dialog. Claude Desktop and Sakana Safe Storage readers route through the same gate.
  • When a refresh fails and none of the card's placed rows have last-good data, the card body becomes an error prompt: the reason plus a Refresh button (the explicit user action that may legitimately show a Keychain approval dialog). Quick links stay behind the caret; the drag preview and Share Screenshot export mirror the card. Once a provider has data, later failures keep the stale rows and report via the header triangle, as before.

Heads-up

  • SecKeychainSetUserInteractionAllowed is deprecated, but it is the only mechanism that actually suppresses classic keychain UI (kSecUseAuthenticationUI and LAContext both fail); the rationale is documented at the gate.
  • While a user-initiated approval dialog is open, background keychain reads pause until it's answered.

Tests

  • New regression tests: gate semantics against the real Security framework (suppression scope, restore, interactive exclusion), non-interactive read behavior, default and CLAUDE_CONFIG_DIR-scoped Claude refresh policy, empty-state detection (placeholder-only and hidden-metric cases), and error-copy formatting. Full suite: 1465 tests passing.
  • Verified live: with a protected extra-account Keychain item, the launch batch resolves in milliseconds with no dialog, and the card shows the new prompt.

https://claude.ai/code/session_01Ur7PxLJ1avetsk3JegAWVY

…ror card

Background and launch refreshes read classic login-keychain items (Claude
Code credentials, Chromium Safe Storage keys) guarded only by
LAContext.interactionNotAllowed, which the classic ACL confirmation dialog
ignores - so macOS could pop password prompts during automatic refreshes
and block them behind the dialog. A process-wide KeychainUISuppression
gate built on SecKeychainSetUserInteractionAllowed now keeps every
non-interactive Security.framework operation prompt-free (a protected item
fails fast into the friendly Permission Required state), while interactive
reads from a manual Refresh hold the gate so a racing background read
cannot disable UI beneath an open approval dialog.

Providers whose refresh failed with no displayable data now replace their
empty "No data" rows with an actionable error card (reason plus a Refresh
button - the one action allowed to show a Keychain prompt), keeping quick
links behind the caret, mirrored in the drag preview and share export.

Claude-Session: https://claude.ai/code/session_01Ur7PxLJ1avetsk3JegAWVY

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: a0953366bd

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread Sources/Runway/Services/SystemClients.swift
…ngage

SecKeychainSetUserInteractionAllowed's return status was ignored: had the
disable call ever failed, the suppression scope would have run its
protected Security.framework call with interaction still enabled - able to
show the exact background dialog the gate exists to prevent. The scope now
reports whether suppression actually engaged; when it did not (logged
loudly), non-interactive bodies skip the prompt-capable call and report
their unavailable outcome instead, and a failed restore is retried before
the next interactive operation so approval prompts cannot stay broken.

Claude-Session: https://claude.ai/code/session_01Ur7PxLJ1avetsk3JegAWVY
@mstallone
mstallone merged commit 958b7b1 into main Jul 30, 2026
1 check passed
@mstallone
mstallone deleted the fix-claude-keychain-bug branch July 30, 2026 22:49
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant