Problem
Provider API keys are stored via Electron safeStorage (apps/desktop/src/main/credential-store.ts), an Electron-only API. The rest of @maka/runtime is pure-Node and Electron-free.
This blocks first-class headless mode (see #31): a pure-Node CLI / eval harness / third-party caller running the same runtime outside Electron cannot decrypt the existing credentials. connectionStore (model/connection config) is already pure-Node in @maka/storage and reusable; only the secret is trapped behind safeStorage.
This is a global, security-sensitive change touching desktop OAuth, connection tests, and bots. It needs owner + collaborator sign-off — not to be implemented unilaterally. This RFC is to align before any code.
Threat model
Per SECURITY.md, the OS user account is the only real enforcement boundary against an adversarial LLM/tool. A tool running as the user can already read files, run commands, hit the network, and read env. Against that threat, at-rest encryption buys little. safeStorage meaningfully protects only secondary threats: offline disk theft, other OS users, backup/cloud-sync leakage, and (macOS) other-app boundary. It is also not cross-platform consistent (Electron docs: Windows DPAPI doesn't stop same-user apps; Linux falls back to basic_text without a secret store).
Prior art (verified)
| Tool |
On disk |
Format |
Perms |
Keychain |
| Codex CLI |
~/.codex/auth.json (file/keyring/auto) |
plaintext JSON |
0600 (on create) |
optional |
| OpenCode |
~/.local/share/opencode/auth.json |
plaintext JSON |
0600 |
no |
| Claude Code |
macOS Keychain; Linux/Win ~/.claude/.credentials.json |
keychain / plaintext |
0600 |
macOS only |
| aider |
env / .env / yaml |
plaintext |
umask |
no |
| gh / aws / docker |
keyring / plaintext / base64 |
mixed |
— |
partial |
The dominant convention for CLI agents is a local 0600 JSON file; keychain-led tools (Claude Code, gh) keep a same-shape 0600 file as the headless fallback.
Proposed design
A single pure-Node CredentialStore interface in @maka/storage, used by desktop + CLI + harness + third-party. Resolution precedence:
env override (MAKA_* / provider-native)
> credential_provider command (first-class extension: 1Password / Vault / keychain)
> JSON file store (default)
> safeStorage one-time importer (migration only)
- File store: user data dir, dir
0700 / file 0600, plaintext (no base64 theater), atomic write, schema version. Windows: ACL or best-effort + clear warning.
credential_provider: returns structured JSON, supports expires_at, has a timeout, must not write secrets to stderr, cached by expiry. This is where keychain / 1Password / Vault live — first-class, not default.
- safeStorage: kept only as a one-time importer — on desktop start, decrypt legacy creds → write into the new store → mark legacy.
- Third-party: callers can inject a custom
CredentialStore (not forced to read the global user file).
Rejected alternatives (with reasons)
- helper-first as default — git/docker/aws/Claude-Code prove helpers are a good extension point, not a good default; wrapping "read a file" in a process protocol adds spawn/timeout/stderr-leak/caching/quoting/diagnostic cost for the common case.
- shell out to OS keychain from Node (
security/secret-tool/cmdkey) — cross-platform semantics inconsistent, can block on user input, Windows has no clean read-to-stdout. OK as an optional helper example, not the core store.
- native keychain lib (keytar archived;
@napi-rs/keyring) — reintroduces native binary / libsecret/DBus / ABI / headless-packaging deps, defeating pure-Node. Optional, never the sole store.
- credential broker over socket — makes headless depend on a running desktop daemon; defeats first-class headless.
Security trade-off (stated honestly)
We trade some at-rest encryption for default composability/debuggability/headless. The main new exposure: a plaintext key getting carried off by iCloud / Time Machine / Dropbox / dotfile sync. Mitigations: keep the store outside common sync paths, document it, unified redaction across logs / crash reports / debug dumps / migration backups.
Open questions
- Do we accept plaintext-at-rest as the default, given the threat model — or require
credential_provider/keychain encryption on by default where available?
- Token model: static API key vs refresh token vs short-lived access token — the store should not treat every secret as a never-expiring string.
- Multi-account / multi-endpoint key shape:
provider + account/profile + base_url/org/tenant.
- Windows permission handling (no
0600 equivalent).
- Migration/rollout: silent import vs explicit user step; what happens to the legacy safeStorage entries.
Driver: #31 · Prior art: Codex CLI, OpenCode, Claude Code, aider · Refs: git credential helpers, aws credential_process, Electron safeStorage
Problem
Provider API keys are stored via Electron
safeStorage(apps/desktop/src/main/credential-store.ts), an Electron-only API. The rest of@maka/runtimeis pure-Node and Electron-free.This blocks first-class headless mode (see #31): a pure-Node CLI / eval harness / third-party caller running the same runtime outside Electron cannot decrypt the existing credentials.
connectionStore(model/connection config) is already pure-Node in@maka/storageand reusable; only the secret is trapped behind safeStorage.This is a global, security-sensitive change touching desktop OAuth, connection tests, and bots. It needs owner + collaborator sign-off — not to be implemented unilaterally. This RFC is to align before any code.
Threat model
Per
SECURITY.md, the OS user account is the only real enforcement boundary against an adversarial LLM/tool. A tool running as the user can already read files, run commands, hit the network, and read env. Against that threat, at-rest encryption buys little. safeStorage meaningfully protects only secondary threats: offline disk theft, other OS users, backup/cloud-sync leakage, and (macOS) other-app boundary. It is also not cross-platform consistent (Electron docs: Windows DPAPI doesn't stop same-user apps; Linux falls back tobasic_textwithout a secret store).Prior art (verified)
~/.codex/auth.json(file/keyring/auto)~/.local/share/opencode/auth.json~/.claude/.credentials.json.env/ yamlThe dominant convention for CLI agents is a local 0600 JSON file; keychain-led tools (Claude Code, gh) keep a same-shape 0600 file as the headless fallback.
Proposed design
A single pure-Node
CredentialStoreinterface in@maka/storage, used by desktop + CLI + harness + third-party. Resolution precedence:0700/ file0600, plaintext (no base64 theater), atomic write,schema version. Windows: ACL or best-effort + clear warning.credential_provider: returns structured JSON, supportsexpires_at, has a timeout, must not write secrets to stderr, cached by expiry. This is where keychain / 1Password / Vault live — first-class, not default.CredentialStore(not forced to read the global user file).Rejected alternatives (with reasons)
security/secret-tool/cmdkey) — cross-platform semantics inconsistent, can block on user input, Windows has no clean read-to-stdout. OK as an optional helper example, not the core store.@napi-rs/keyring) — reintroduces native binary / libsecret/DBus / ABI / headless-packaging deps, defeating pure-Node. Optional, never the sole store.Security trade-off (stated honestly)
We trade some at-rest encryption for default composability/debuggability/headless. The main new exposure: a plaintext key getting carried off by iCloud / Time Machine / Dropbox / dotfile sync. Mitigations: keep the store outside common sync paths, document it, unified redaction across logs / crash reports / debug dumps / migration backups.
Open questions
credential_provider/keychain encryption on by default where available?provider + account/profile + base_url/org/tenant.0600equivalent).Driver: #31 · Prior art: Codex CLI, OpenCode, Claude Code, aider · Refs: git credential helpers, aws credential_process, Electron safeStorage