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 do not 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 (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.
Headless fallback — a TypeScript/Express daemon (src/server) speaks the same API on the same port for CI environments where the native app cannot run. A parity check verifies the JS daemon and the Swift router behave identically on shared endpoints.
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. Native-app unlock refreshes the CLI session file so the CLI and MCP pick up the new bearer without re-prompting.- JS SDK (
packages/sdk) — same API, programmatic. CLI + SDK wired for external publish via GitHub Packages. vault-captureMCP 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. Onevault unlockcovers MCP, CLI, and SDK.
VaultKeychainKit — Drop-in for Swift Apps
A SwiftPM package at packages/swift/VaultKeychainKit/ lets Swift apps the user writes route their KeychainHelper.read / write / delete(account:) call sites through the vault, with an opt-in local-Keychain cache for offline. Opt-in only — no Keychain federation; an app explicitly adopts. The scan-xcode tool flags Xcode projects whose Keychain call sites would benefit, and live integration tests run against a real vault. Adoption guide: docs/vault-keychain-kit.md.
Transcript Audit
A discoverer for leaked credentials sitting in coding-agent transcripts. The audit opens ~/.claude/projects/*/*.jsonl read-only and stores metadata only by default: kind, ≤12-char preview, SHA-256 hash, first/last-seen, project link. The leaked value itself is never persisted. Pass --store-values only when you need to re-use the leaked value for rotation. Same detector implemented in TypeScript and Swift sharing one regex source; surfaced in the app’s Discovered tab and via three MCP tools added to vault-capture.
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 - Pre-commit git-leak hook + scan-secrets hook with direct-capture for high-confidence patterns
- Rotation reminder plist + endpoint + check script for high-risk credentials