Credential Prompts Are Side Effects
Credential prompts are side effects
A credential prompt is not a read. It is an interaction with the user's desktop.
That distinction matters most in daemon-backed apps. A background process can think it is only checking whether a token exists, but the OS can turn that check into a modal keychain prompt. If the user is away, every retry becomes another demand for attention.
The rule
Keep prompt-capable credential reads out of unattended startup and polling
paths. If a background process still discovers that it needs the user, that
fact becomes state.
Do not keep asking. Latch the auth-required state, notify once, fail fast, and wait for an explicit repair action such as login, reauth, or unlock.
What Spotuify taught us
spotuify used to have two separate auth lessons tangled together:
- refresh tokens are mutable shared state;
- OS credential prompts are interactive side effects.
The refresh-token fix made token rotation safe. The prompt-storm fix made auth failure humane.
The current shape is better:
- Try the local disk auth mirror first.
- Touch the system keychain only when the non-interactive path cannot answer.
- Treat an unanswered or user-needed keychain prompt as
AuthRequired. - Latch that state in the daemon.
- Emit one auth event and one desktop notification for that error kind.
- Let health checks fail fast while the latch is active.
- Clear the latch only after a non-interactive recovery probe or explicit login succeeds.
What mxr reinforced
In mxr 0.6.10, constructing a password-backed IMAP account eagerly read the
macOS Keychain. After a binary upgrade changed the ad-hoc signing identity, the
keychain asked for interactive approval. The daemon failed before its socket was
ready, so every client lost access because one account needed attention.
The 0.6.11 and 0.6.12 fixes changed the dependency, not just the error message:
- IMAP/SMTP passwords are written to a
0600secrets.tomlfile. - Password reads check disk before the keychain.
- A keychain hit is migrated to disk for later reads when that write succeeds.
- Password lookup happens when an account connects, not while the daemon boots.
mxr accounts repaircan run without a healthy daemon.MXR_KEYCHAIN=offmakes disk the only IMAP/SMTP password source.
The tradeoff is explicit: filesystem permissions replace OS encryption for
IMAP/SMTP passwords. In return, a binary upgrade cannot turn a background
startup read into a modal prompt that bricks the whole mail runtime.
The broader lesson is not that every app should copy this storage choice. It is
that an interactive credential backend should not be a global startup
dependency. One broken account should fail its own operation and leave the
daemon, diagnostics, other accounts, and repair path available.
The pattern
Use this for any local app that mixes a daemon, one-shot commands, and OS credentials:
- Put a non-interactive credential source in front of the interactive store.
- Resolve credentials at the narrow operation that needs them, not during global startup.
- Keep diagnostics and repair available when provider auth is broken.
- Bound any unavoidable credential-store read with a timeout.
- Classify "user approval needed" separately from network, provider, and parsing errors.
- Store that classification in the long-lived owner, usually the daemon.
- Deduplicate user-facing notifications by error kind.
- Make the repair path explicit and idempotent.
The key move is ownership. The daemon owns auth-required state because it owns
runtime truth. A CLI or TUI can display it, but it should not rediscover it by
poking the keychain again. If the daemon itself cannot start, the repair command
needs a smaller dependency path that can operate directly on configuration and
credential storage.
What not to do
- Do not retry an interactive credential read from a background health loop.
- Do not read every account's credentials before binding the daemon socket.
- Do not treat a keychain prompt timeout as a transient provider error.
- Do not send a desktop notification on every failed poll.
- Do not make the TUI responsible for suppressing daemon auth behavior.
- Do not hide the repair command in prose. Make it a real command.
Why this generalises
This applies to more than Spotify:
- Gmail and Microsoft OAuth daemons with refresh tokens in a system keychain.
- Cloud CLIs that auto-refresh credentials from a desktop credential helper.
- Local agent runtimes that share account credentials across helper processes.
- Desktop apps with menu-bar helpers, background sync, or local MCP servers.
Any place where "check credentials" can show a prompt needs this rule.
Code evidence
In spotuify:
crates/spotuify-spotify/src/auth.rsreads the disk token mirror before the macOS Keychain and maps user-needed keychain access toAuthRequired.crates/spotuify-daemon/src/state.rslatchesAuthRequired, skips further interactive reads while the latch is active, and clears the latch only when disk-backed recovery is available.crates/spotuify-system/src/notifications.rsdeduplicates auth-error notifications by error kind.
In mxr:
crates/daemon/src/provider_credentials.rsresolves IMAP/SMTP passwords lazily and disk-first.crates/daemon/src/state.rstests that an unreadable credential does not prevent daemon construction.crates/daemon/tests/accounts_repair_cli.rsproves repair can persist credentials without a running daemon.
The useful lesson is not "never use the keychain." It is "never let a background loop repeatedly ask the user the same question."