Your key now survives an app restart. On iOS 16+ and Android API 26+ the pasted key is written to the platform keychain, so closing and reopening the app no longer shows Not set — you enter it once. Before this,
createWebSecrets() (a module-scoped Map) was handed to the phone too, so the key was gone on every launch.Quick Start
1
Open Settings and find the OpenAI API key row
Under the Engine section, the OpenAI API key row shows Not set, a masked field, and a Remove button. On a platform without a hardware keychain (the web adapter, where
isHardwareBacked === false), a warning above the rows says secrets are kept in app memory.2
Paste your key
Paste the key into the masked field and blur it or press Enter. The row flips to Configured, the field returns to its placeholder, and nothing is echoed back.
3
Send the very next message
No relaunch. The in-process engine reads the key on every turn, so the next message you send is authenticated.
4
Rotate or revoke
To rotate, paste a new value over the old one — the replacement also survives a restart. To revoke, press Remove — the row returns to Not set and the keychain entry is gone.
How It Works
A pasted key is committed as aset-secret intent, written straight to SecretsPort, and read back by the engine on the next turn.
The field carries data-action="set-secret" and its key; a root-delegated change listener decodes it through intentFrom and calls facade.setSecret(ref, value). On the next turn, createInProcessEngine calls apiKeyFor(secrets, settings.defs()) and builds the agent with the key it finds.
Where set(ref, value) lands depends on the platform, both reached through src-tauri/plugins/secrets:
What the field does and does NOT do
The masked field is governed by three rules fromsecretControls in app/src/main.ts, and each has a broken version that looks completely normal on screen.
Configuration Options
The row is declared by oneSettingDef in SETTING_DEFS (source: app/src/registry.ts).
One slot, not five.
createInProcessEngine builds exactly one kind of agent, routed through OpenAIService, so openai is the only slot with a reader. The other four slots — anthropic, google, openrouter, custom — stay available for the commit that adds a provider setting and the code that honours it, in that order. An anthropic row today would be a declared-but-unread setting (the #4636 defect).Common Patterns
An empty commit is refused, and the value is trimmed — fromintentFrom in app/src/intents.ts.
createInProcessEngine in app/src/main.ts.
enginesFor already learned this with baseUrl, and it is worse for a credential — the failure it produces is the same missing-key error the user was trying to clear.
Best Practices
Rotate through Settings, not by editing files
Rotate through Settings, not by editing files
The key lives in the iOS keychain or Android keystore, never in the plaintext settings file. Paste a new value over the old one to rotate; press Remove to revoke.
Trust the presence label, not the field
Trust the presence label, not the field
The field is empty on every paint —
syncSecret empties it again after every commit. Configured / Not set on the presence node is the truth about whether a key is stored; the field only ever holds what you are typing right now.A masked echo would lie — that is why there is none
A masked echo would lie — that is why there is none
Showing dots for a stored key would read as “a key is already here”, which is what the presence label is for. The field paints empty precisely so it never claims a key is present when it may not be.
An absent key is omitted, not sent as empty
An absent key is omitted, not sent as empty
When no key is set,
apiKey is left off the agent config rather than passed as null or "". Upstream treats a falsy apiKey as “fall back to the environment”, and a phone has no process.env — so the honest shape for “no key” is an absent field and the provider’s own missing-credential error.What if my key is gone after an Android backup restore?
What if my key is gone after an Android backup restore?
Enter it once more and it sticks. Google’s auto-backup copies the encrypted preferences file onto a new device, but the
AndroidKeyStore master key that decrypts it is device-local and does not travel — so the old file cannot be read. The plugin catches that KeyStoreException, deletes the unreadable file with deleteSharedPreferences, and recreates the store empty, so the settings screen is never bricked. Any pre-restore secrets are lost and re-entered once, exactly like a fresh install. Source: SecretsPlugin.kt, store() / discard().Related
Settings Screen
How a secret row renders, commits, and refreshes its presence.
Storage & Secrets
SecretsPort, readSecretSetting, and the isHardwareBacked warning.Native Secrets
The keychain plugin behind this row, and why it refuses on an unsupported platform.
Engines
How the in-process engine reads its key on every turn.
i18n & A11y
How the four secret strings are announced.

