Secrets Vault
Native macOS secrets manager. AES-256-GCM, Touch ID unlock, loopback API. Lets CLIs and agents inject values without ever seeing them.
The Problem
Every coding agent eventually wants secrets — API keys, OAuth tokens, signing certs. The default options are bad:
.envfiles — plaintext, get committed by accident, leak via screenshots and shoulder surfing- System keychain — solid storage, but apps and CLIs have to be individually authorized and most agent frameworks don’t bother
- Cloud password managers — great for humans, awkward for headless agents, and the values still hit clipboards and screen-readable surfaces
- Pasting into chat — the worst option, and the one that happens most often
What I wanted: a local vault that an AI agent can use without ever seeing. The agent should be able to list secrets, ask for one to be injected into a specific file, get audit-logged feedback that the operation happened — but never receive the value as text in its context window.
What I Built

A native macOS app with a loopback HTTP API and a CLI/SDK/MCP triad on top of it. Setup is a master password (used to derive the encryption key via scrypt) and an optional Touch ID enrollment for unlock. The password is never stored — only a verification hash.

Architecture
SwiftUI app — the canonical surface. Calm Precision design system: stone-50/violet-600 light, stone-950/violet-400 dark, adaptive via NSColor(name:dynamicProvider:).
In-process HTTP server — Network.framework NWListener bound to 127.0.0.1:4100, loopback-only via NWParameters.acceptLocalOnly, with origin and x-vault-client header checks on every state-changing request. This is the integration point everything else builds against.
Encryption — Apple’s CryptoKit. Master password → scrypt KDF → AES-256-GCM key. The derived key lives only in SessionManager memory, gets zeroed on lock or 30-minute idle.
Storage — SQLite at ~/Library/Application Support/com.secretsvault.app/vault-data/vault.db. Encrypted blobs only. An append-only audit table records every access — the table never holds secret values, only the operation, ID, timestamp, and client.
Auth — Touch ID via LAContext and the biometric Keychain (which requires real Apple Developer signing + Team ID + a keychain-access-groups entitlement — not ad-hoc-signable). Master password is the fallback. Platform passkey support is wired but waiting on a real webcredentials: associated domain.
CLI, SDK, MCP
The app exposes one HTTP API; three clients consume it:
vaultCLI (packages/cli) —vault unlock,list,get <id> --value,create,inject.unlockdefaults to Touch ID against the native app, caches a bearer token at~/.secrets-vault/session.- JS SDK (
packages/sdk) — same API, programmatic. - vault-capture MCP plugin (
plugins/vault-capture/) — Claude Code / Codex MCP server. Picks up the bearer token in this order:$VAULT_CAPTURE_TOKEN→~/.config/vault-capture/token→ the CLI’s session. So onevault unlockcovers MCP, CLI, and SDK.
A headless TypeScript/Express daemon (src/server) speaks the same API on the same port for CI environments where the native app can’t run.
The Agent-Safe Path
The MCP and capture endpoints return metadata only — ID, name, last-used timestamp, never the value. When an agent needs a secret in a file, it calls inject, which writes the value directly to a path-validated .env file on disk. The value never enters the agent’s context, never gets logged, never appears in transcripts. The audit log shows the agent asked for it; the agent itself never knew it.
Security Posture
- Secret values never appear in error messages or logs
- Derived encryption key never touches disk
- Every value access flows decrypt → audit-log → return
- Loopback-only; no network surface
- Bearer-token sessions, zeroed on lock or idle
- The app uses
NSWindowSharingNoneso screen-recording tools and screenshots return blank windows for the vault UI itself