Install
openclaw skills install api-key-ui-tabVault-backed API Keys management for OpenClaw. Secure file-based secret storage with one-click migration from plaintext config, dynamic key discovery, vault key selector for skills, manual secret creation, and plugin-registered settings tab.
openclaw skills install api-key-ui-tabVault-backed API key management for the OpenClaw Control dashboard. Keys are stored in a secure file (~/.openclaw/secrets.json, mode 0600) and referenced via OpenClaw's built-in Secrets System. The AI agent never sees your keys.
| Component | Status |
|---|---|
| Vault File Storage | ✅ Working |
| Secret References (SecretRef) | ✅ Working |
| Dynamic Key Discovery | ✅ Working |
| One-Click Migration | ✅ Working |
| Plugin-Registered Tab | ✅ Working |
| Vault Status Banner | ✅ Working |
| Key Status Badges | ✅ Working |
| Vault-Only Keys Section | ✅ Working |
| Manual "+ Add Secret" Form | ✅ Working |
| Restart Notification Banner | ✅ Working |
| Skills Vault Key Selector | ✅ Working |
| Skills Inline Key Creation | ✅ Working |
| Auth Profiles Display | ✅ Working |
Keys are stored in ~/.openclaw/secrets.json (file permissions 0600). When you save a key, the UI:
openclaw.json (if not already present)SecretRef objectConfig before migration:
{
"env": {
"OPENAI_API_KEY": "sk-proj-abc123..."
}
}
Config after migration:
{
"env": {
"OPENAI_API_KEY": { "source": "file", "provider": "default", "id": "/OPENAI_API_KEY" }
},
"secrets": {
"providers": {
"default": { "source": "file", "path": "~/.openclaw/secrets.json", "mode": "json" }
},
"defaults": { "file": "default" }
}
}
Vault file (~/.openclaw/secrets.json):
{
"OPENAI_API_KEY": "sk-proj-abc123..."
}
The "🔒 Migrate to Vault" button appears when plaintext keys are detected. It:
openclaw.json for all plaintext API key valuesSecretRef objects in configThe UI automatically scans the entire config for API keys — no hardcoded list.
Detection patterns: apiKey, api_key, token, secret, *_KEY, *_TOKEN, *_SECRET
Where it looks:
env.* — Environment variablesskills.entries.*.apiKey — Skill-specific keysmessages.tts.*.apiKey — TTS provider keysKnown providers get friendly names, descriptions, and "Get key ↗" links:
| Provider | Env Key |
|---|---|
| Anthropic | ANTHROPIC_API_KEY |
| OpenAI | OPENAI_API_KEY |
| Google / Gemini | GOOGLE_API_KEY / GEMINI_API_KEY |
| Brave Search | BRAVE_API_KEY |
| ElevenLabs | ELEVENLABS_API_KEY |
| Deepgram | DEEPGRAM_API_KEY |
| OpenRouter | OPENROUTER_API_KEY |
| Groq | GROQ_API_KEY |
| Fireworks | FIREWORKS_API_KEY |
| Mistral | MISTRAL_API_KEY |
| xAI (Grok) | XAI_API_KEY |
| Perplexity | PERPLEXITY_API_KEY |
| GitHub | GITHUB_TOKEN |
| Hume AI | HUME_API_KEY / HUME_SECRET_KEY |
Top of the page shows:
Each key row shows:
Keys stored in the vault that aren't referenced by any config path are displayed in a dedicated "Vault-Only Keys" card. These are keys created manually or by skills that don't have a corresponding env/config entry. Each shows:
The Vault tab header includes a "+ Add Secret" button that expands an inline form:
envEntry: false — no config entry created, no restart triggeredWhen a vault write triggers a config change, a yellow warning banner appears:
⚠ New secrets require a gateway restart to take effect. [Restart Now]
The banner persists until the user clicks "Restart Now" or refreshes. This replaces the previous auto-reload behavior that caused unexpected gateway restarts.
On the Skills tab, skills that declare a primaryEnv get a vault key selector instead of a raw password input:
When unlinked:
SecretRef to skills.entries.<key>.apiKey in configWhen linked:
🔒 KEY_NAME with an "Unlink" buttonInline key creation:
All skill groups (workspace, built-in, managed) render expanded (<details open>) for better discoverability. Previously workspace and built-in were collapsed by default.
Auth profile keys (from auth-profiles.json) that are stored in the vault are listed with their status. Backend RPCs support listing, error reset, and deletion.
┌──────────────────────────────────────────────────────────┐
│ Browser │
│ ┌──────────────────────────────────────────────────┐ │
│ │ Vault Tab │ │
│ │ ┌──────────────────────────────────────────┐ │ │
│ │ │ OpenAI: [••••••••••] [Save] [✕] 🟢VAULT │ │ │
│ │ │ Anthropic: [ ] [Save] ⚪NOT SET│ │ │
│ │ │ + Add Secret [KEY_NAME] [value] [Save] │ │ │
│ │ └──────────────────────────────────────────┘ │ │
│ │ │ │
│ │ Skills Tab │ │
│ │ ┌──────────────────────────────────────────┐ │ │
│ │ │ whisper-api: 🔒 OPENAI_API_KEY [Unlink] │ │ │
│ │ │ sag: [Select vault key ▾] │ │ │
│ │ └──────────────────────────────────────────┘ │ │
│ └──────────────────────────────────────────────────┘ │
│ │ │
│ ▼ (direct RPC, not via agent) │
└───────────────────────────┼───────────────────────────────┘
│
┌───────▼───────┐
│ Gateway │
│ secrets.write │
│ skills.update │
└──┬─────────┬──┘
│ │
┌────────▼──┐ ┌──▼────────────┐
│ secrets. │ │ openclaw.json │
│ json │ │ (SecretRef │
│ (0600) │ │ objects only) │
└────────────┘ └────────────────┘
| Method | Description |
|---|---|
secrets.status | Vault file status, key count, plaintext count |
secrets.list | List secret IDs with masked values |
secrets.write | Store key in vault + optionally update config with SecretRef. envEntry param (default true) controls whether an env block entry is created. Returns restartNeeded flag instead of auto-reloading. |
secrets.delete | Remove from vault + config |
secrets.migrate | Batch-migrate all plaintext keys to vault |
secrets.authProfiles.list | List auth profile keys with vault status |
secrets.authProfiles.resetErrors | Reset auth profile error state |
secrets.authProfiles.delete | Delete an auth profile |
skills.update | Updated with vaultKeyId param — writes a SecretRef to skills.entries.<key>.apiKey or unlinks (empty string) |
SkillStatusEntry includes a vaultKeyId field that reads the raw config JSON (not the runtime-resolved config where SecretRefs are replaced with resolved strings). This is done via extractVaultKeyIdFromConfig() which reads and caches openclaw.json directly, checking for SecretRef objects in skills.entries.<key>.apiKey.
No auto-restart on vault save. Previously, secrets.write called reloadSecrets() which could trigger a gateway restart. Now:
envEntry: false) don't touch config — no restart neededrestartNeeded: true — UI shows a restart bannerwriteConfigFile() — triggers the config file watcher which causes a gateway restart (inherent to the config watcher system)This skill uses OpenClaw's built-in Secrets System (src/secrets/):
{ source: "file", path: "~/.openclaw/secrets.json", mode: "json" }{ source: "file", provider: "default", id: "/<KEY_NAME>" }prepareSecretsRuntimeSnapshot() resolves all refs at gateway startupThe secrets system also supports env and exec providers for advanced setups (e.g., environment variables, external vault commands). The file provider is the default for this UI.
| File | Purpose |
|---|---|
src/gateway/server-methods/secrets.ts | Vault RPCs (status, list, write, delete, migrate, authProfiles) |
src/gateway/server-methods/skills.ts | Skills update with vaultKeyId param |
src/gateway/server-methods/plugins-ui.ts | Plugin view registration |
src/gateway/protocol/schema/agents-models-skills.ts | vaultKeyId in SkillsUpdateParamsSchema |
src/agents/skills-status.ts | vaultKeyId field on SkillStatusEntry, raw config reader |
ui/src/ui/controllers/apikeys.ts | Vault-aware state, addVaultSecret, loadVaultOnlyKeys |
ui/src/ui/controllers/skills.ts | VaultKeyEntry type, loadVaultKeys, linkSkillToVaultKey, addVaultKeyAndLink |
ui/src/ui/views/apikeys.ts | Vault UI (banners, badges, migration, add form, vault-only keys, restart banner) |
ui/src/ui/views/skills.ts | Vault key selector dropdown, inline creation, expanded groups |
ui/src/ui/app.ts | State properties (vault, restart, skill key management) |
ui/src/ui/app-render.ts | Prop wiring for vault and skills |
ui/src/ui/app-settings.ts | Tab load triggers for vault keys |
ui/src/ui/types.ts | vaultKeyId on SkillStatusEntry |
ui/src/ui/navigation.ts | Vault tab (lock icon), removed 1password/discord standalone tabs |
apikeys-ui/
├── SKILL.md # This file
├── INSTALL_INSTRUCTIONS.md # Step-by-step installation (legacy)
└── reference/
├── apikeys-controller.ts # UI controller (vault tab)
├── apikeys-views.ts # UI view (vault tab)
├── secrets-rpc.ts # Backend vault RPCs
├── skills-controller.ts # UI controller (skills vault integration)
├── skills-views.ts # UI view (vault key selector)
├── skills-status.ts # Backend skill status with vaultKeyId
└── skills-rpc.ts # Backend skills update RPC
No auto-restart on save — secrets.write no longer calls reloadSecrets(). A restart banner with "Restart Now" button lets the user decide when to restart.
Vault-only keys — Keys created with envEntry: false don't appear in config. A separate secrets.list call finds all vault entries and shows orphan keys in a dedicated section.
Raw config reading for vaultKeyId — The runtime resolves SecretRefs to strings, so loadConfig() returns resolved values. extractVaultKeyIdFromConfig() reads the raw JSON file directly (with mtime caching) to detect SecretRef objects.
Skills expanded by default — All <details> groups render open for better UX. Collapsed-by-default hid skills that needed configuration.
Skills use vault references, not plaintext — Skills with primaryEnv get a vault key selector dropdown instead of a password input. Linking writes a SecretRef to skills.entries.<key>.apiKey in config.
Inline key creation from skills — "+ Add new vault key…" in the skills dropdown creates a vault entry and links it in one step, reducing friction.
primaryEnvenvEntry param on secrets.write — controls whether an env block entry is createdvaultKeyId param on skills.update — writes SecretRef or unlinks skill from vault key~/.openclaw/secrets.json (mode 0600) instead of plaintext configsecrets.status, secrets.list, secrets.write, secrets.delete, secrets.migrateconfig.patch