Back to projects
Active Started Jan 2026

Secrets Vault

Native macOS secrets manager. AES-256-GCM, Touch ID unlock, loopback API. Lets CLIs and agents inject values without ever seeing them.

Private Repo
SwiftUI CryptoKit Network.framework SQLite Touch ID / LAContext MCP

The Problem

Every coding agent eventually wants secrets — API keys, OAuth tokens, signing certs. The default options are bad:

  • .env files — 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

Secrets Vault unlocked — secrets list with search, detail pane showing "USE IN CLAUDE CODE" reference card. The secret ID and inject endpoint are visible; the value itself never appears anywhere in the UI or the API responses an agent receives ::border

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.

First-run setup — Create Your Vault. The master password derives the encryption key via scrypt; only a verification hash is persisted ::border

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 serverNetwork.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:

  • vault CLI (packages/cli) — vault unlock, list, get <id> --value, create, inject. unlock defaults 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 one vault unlock covers 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 NSWindowSharingNone so screen-recording tools and screenshots return blank windows for the vault UI itself