How it works
import { Aside } from ‘@astrojs/starlight/components’;
OpaqueVault is built around one principle: the server should never be able to decrypt your secrets, and the AI should never see them either. Everything follows from that.
The two-process model
Section titled “The two-process model”Claude Code ──MCP/stdio──▶ ov mcp serve ──HTTPS+PQC──▶ api.opaquevault.comThree actors. Each has a strictly limited role:
Claude Code (or any MCP client)
Section titled “Claude Code (or any MCP client)”- Calls MCP tools by name:
vault_run,vault_list_secrets,vault_status - Never requests secret values — only the ability to act with them
- Receives exit codes, stdout/stderr, secret names — never plaintext values
- If a raw secret accidentally enters the MCP channel, the interceptor blocks it
ov mcp serve — the local bridge
Section titled “ov mcp serve — the local bridge”- Runs on your machine as a stdio MCP server
- Holds your Key Encryption Key (KEK) in memory, derived from your master password
- Fetches encrypted blobs from the API, decrypts them locally
- Injects plaintext values as env vars into subprocesses — values exist in memory for milliseconds, then are zeroed
- Runs the MCP context interceptor: scans all inbound MCP messages for secret patterns before they reach Claude
api.opaquevault.com — the blob store
Section titled “api.opaquevault.com — the blob store”- Stores only ciphertext — it has no decryption capability
- No decrypt endpoint. No key escrow. No master password.
- A fully compromised server exposes nothing useful — all blobs require your KEK to decrypt
What happens when Claude calls vault_run
Section titled “What happens when Claude calls vault_run”- Claude calls
vault_run({ command: "go run ./cmd/migrate", secrets: ["DATABASE_URL"] }) ov mcp servereceives the call- Bridge fetches the encrypted blob for
DATABASE_URLfrom the API over ML-KEM-768 + X25519 hybrid TLS - Bridge decrypts the DEK using the KEK in memory, then decrypts the ciphertext using the DEK
- Subprocess spawns with
DATABASE_URL=postgres://...in its environment - DEK and plaintext are zeroed from memory
- MCP response returns:
{ exit_code: 0, stdout: "migrations: 3 applied", stderr: "" } - Claude receives the response — no secret value anywhere in it
The interceptor
Section titled “The interceptor”The interceptor is a middleware layer inside ov mcp serve that scans every MCP message flowing through the bridge — both directions.
What it detects: Known secret formats (AWS access keys, GitHub tokens, Stripe keys, private keys, JWTs, connection strings) plus high-entropy strings that match no known format.
What it does when it fires:
- BLOCK mode (default): drops the message, returns a structured error to Claude:
"[OpaqueVault] Potential secret intercepted (aws-access-key). Message blocked. Store this value with 'ov secret set' instead." - WARN mode (
OV_INTERCEPT_MODE=warn): allows the message through but writes aninterceptevent to the audit log
Why this matters: Even disciplined developers make mistakes. The interceptor makes the zero-knowledge guarantee unconditional — it can’t be bypassed by user error. No other secret manager sits in this path.
The encryption stack
Section titled “The encryption stack”master password ↓ Argon2id (time=1, mem=64MB, threads=4)KEK — 32 bytes, lives only in client memory, never transmitted ↓ AES-256-GCM + random nonceDEK — 32 random bytes per secret, zeroed after use ↓ AES-256-GCM + random nonceciphertext — all the server ever storesTransport uses ML-KEM-768 + X25519 hybrid KEM (NIST FIPS 203). If either primitive is broken, the other protects you. There is no X25519-only fallback. No downgrades.
ov scan — finding secrets before they leak
Section titled “ov scan — finding secrets before they leak”ov scan is a local repo scanner. No account required, no network calls.
ov scan .It walks your git-tracked files and detects: AWS access keys, GitHub tokens, Stripe keys, private keys, JWT tokens, Postgres/MySQL/MongoDB connection strings, and high-entropy blobs.
ov scan . --fix# prompts to store found secrets in OpaqueVault,# then prints git filter-repo cleanup instructionsov scan uses the same detection engine (internal/detect/) as the MCP interceptor — the same patterns that catch accidental pastes in Claude Code also catch committed secrets in your repo.
Security invariants
Section titled “Security invariants”These rules are enforced in code and verified by tests:
- KEK never leaves the client
- Plaintext secret values never appear in MCP responses
- Nonces are never reused —
crypto/rand.Readfor every operation - Master password is never persisted — only the derived KEK (session only)
- DEKs are zeroed after use
- No
get_secretMCP tool — ever - The API has no decrypt endpoint
- Argon2id parameters are hardcoded client-side — not negotiated with the server
- ML-KEM-768 is always part of the hybrid — no X25519-only fallback
- Audit log secret references are HMAC hashed — not plaintext IDs or names