Root cause of 'OAuth session expired and could not be refreshed': a host-run
(non-containerized) claude turn read claude's OWN credential store (macOS
Keychain / ~/.claude), not the login the user signed into through Nucleic — so a
stale external login failed. The broker only injected the token for containerized
turns. Now the host spawn sets CLAUDE_CODE_OAUTH_TOKEN from the broker's
freshAccessToken() (refreshed on demand), via a token-provider closure wired from
the app; container turns are unchanged.
Also hide auth artifacts a completed sign-in made stale: the 'Unauthenticated —
log in' row + Log in button and the 'Opening … sign-in in your browser…' note are
dropped once a newer 'sign-in complete' note exists (a later lapse still shows the
prompt). The completion note is kept.
The login affordance never appeared: the ephemeral/scratch transcript passed no
onLogin, and a fatal auth failure arrives as runFinished(.errored) with the reason
in finalText (no .error event) so AuthErrorRow never fired in the main view.
- Wire a backend-aware onLogin into the ephemeral transcript.
- Render AuthErrorRow for a runFinished(.errored) whose finalText looks like an
auth failure, so the main + scratch views both show the button.
- Broaden isAuthError (oauth session expired / failed to authenticate / run /login).
- AppStore.login(forBackend:) routes to Claude or Codex OAuth.
- Settings > Agents: add third 'Unauthenticated' state (orange) with a Log in
button, alongside Connected/Not Connected.
Suppressing the prompt was a band-aid; the root cause is that ClaudeLoginKeychain
read Claude Code's foreign 'Claude Code-credentials' item on every read. Now that
Nucleic mediates its own OAuth, it has no reason to touch another app's credential.
Remove the external read, adoptExternalLoginAfterExplicitLogin, preferredLogin, and
the legacyService constant; read() returns Nucleic's own item only. Nucleic now
reads exclusively credentials it minted, so it can never raise a cross-application
Keychain prompt.
The legacy login-Keychain ACL/partition-list panel ("<app> wants to use
'Claude Code-credentials'…") is not governed by kSecUseAuthenticationUIFail,
so Nucleic Dev.app prompted when reading Claude Code's item. Wrap every legacy
read in SecKeychainSetUserInteractionAllowed(false) so it fails silently instead
of prompting; an already-trusted item still reads without UI. Drop the redundant
(and deprecated) kSecUseAuthenticationUIFail flag.
Stop sandbox/control containers from inheriting ambient ANTHROPIC_API_KEY/
OPENAI_API_KEY/CODEX_API_KEY (which override the mediated OAuth login); keep
the in-Nucleic API-key stores. Scrub the same keys from host_exec subprocesses
via ProcessSpec.removeFromEnvironment. Agent-spawned containers/VMs already
receive no credentials.