Skip to content

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.


Claude Code ──MCP/stdio──▶ ov mcp serve ──HTTPS+PQC──▶ api.opaquevault.com

Three actors. Each has a strictly limited role:

  • 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
  • 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
  • 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

  1. Claude calls vault_run({ command: "go run ./cmd/migrate", secrets: ["DATABASE_URL"] })
  2. ov mcp serve receives the call
  3. Bridge fetches the encrypted blob for DATABASE_URL from the API over ML-KEM-768 + X25519 hybrid TLS
  4. Bridge decrypts the DEK using the KEK in memory, then decrypts the ciphertext using the DEK
  5. Subprocess spawns with DATABASE_URL=postgres://... in its environment
  6. DEK and plaintext are zeroed from memory
  7. MCP response returns: { exit_code: 0, stdout: "migrations: 3 applied", stderr: "" }
  8. Claude receives the response — no secret value anywhere in it

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 an intercept event 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.


master password
↓ Argon2id (time=1, mem=64MB, threads=4)
KEK — 32 bytes, lives only in client memory, never transmitted
↓ AES-256-GCM + random nonce
DEK — 32 random bytes per secret, zeroed after use
↓ AES-256-GCM + random nonce
ciphertext — all the server ever stores

Transport 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.

Terminal window
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.

Terminal window
ov scan . --fix
# prompts to store found secrets in OpaqueVault,
# then prints git filter-repo cleanup instructions

ov 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.


These rules are enforced in code and verified by tests:

  1. KEK never leaves the client
  2. Plaintext secret values never appear in MCP responses
  3. Nonces are never reused — crypto/rand.Read for every operation
  4. Master password is never persisted — only the derived KEK (session only)
  5. DEKs are zeroed after use
  6. No get_secret MCP tool — ever
  7. The API has no decrypt endpoint
  8. Argon2id parameters are hardcoded client-side — not negotiated with the server
  9. ML-KEM-768 is always part of the hybrid — no X25519-only fallback
  10. Audit log secret references are HMAC hashed — not plaintext IDs or names