Skip to main content
Paste an OpenAI key into the Settings screen and the in-process engine picks it up on the very next message — no relaunch, and the key never touches the settings file.
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 a set-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 from secretControls in app/src/main.ts, and each has a broken version that looks completely normal on screen.

Configuration Options

The row is declared by one SettingDef 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 — from intentFrom in app/src/intents.ts.
The engine reads the key on every turn, not at boot — from createInProcessEngine in app/src/main.ts.
The engine is built once and held for the session, so a construction-time read would need a force-quit to pick up a pasted key. 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

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.
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.
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.
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.
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().

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.