If you’re using Claude Code and wondering where to store your API keys, database passwords, and other secrets — this is the page you need. The short answer: not in .env. Here’s the right approach and how to set it up.
Disclosure: We built OpaqueVault to solve this problem. This guide uses OpaqueVault as the specific solution.
The .env problem is covered in depth in Why your AI coding assistant is a secret leak waiting to happen. Short version: Claude Code is a filesystem agent running with your full user permissions. During debugging and development, it reads .env files — often without asking, as a natural part of understanding your project. When that happens, secrets enter its context window and can persist in conversation history. There is no documented per-message redaction of API keys or credentials before they reach the model.
The fix requires a different storage layer: one that Claude can use but cannot read. OpaqueVault is a local-first vault with optional cloud sync — secrets are encrypted on your machine before anything leaves it, and the remote server stores only ciphertext it cannot decrypt. The architecture and the local process (ov mcp serve) are what matter for the security guarantee; the cloud component is just durable encrypted storage.
The right approach: MCP vault injection
An MCP vault manager sits between Claude and your credentials. Claude asks the vault to run commands on its behalf. The vault decrypts secrets locally and injects them into the subprocess environment. Claude gets the command output. The secret value never enters Claude’s context.
OpaqueVault is built specifically for this pattern. Here’s how to set it up.
Step-by-step: storing secrets for Claude Code
1. Install OpaqueVault
brew install opaquevault-brew/tap/ovOn Linux or without Homebrew:
curl -fsSL https://install.opaquevault.com | sh2. Initialize your vault
ov vault initYou’ll be prompted to set a master password. This is the only password you need to remember. It derives your Key Encryption Key (KEK) via Argon2id — the KEK never leaves your machine. Each secret gets its own randomly generated Data Encryption Key (DEK), encrypted under your KEK. The server stores only the encrypted DEK and encrypted secret value — it has no way to decrypt either. See the security model for full details including memory handling and session behavior.
3. Store your secrets
Use the app/environment/name format:
# Databaseov secret create my-app/development/DATABASE_URL
# Stripe (secret key only — publishable keys are safe to put in source code)ov secret create my-app/development/STRIPE_SECRET_KEY
# OpenAI (if your app calls it directly)ov secret create my-app/development/OPENAI_API_KEY
# Any other serviceov secret create my-app/development/SENDGRID_API_KEYEach command prompts for the value. The value is encrypted client-side before anything is sent to the server.
4. Configure OpaqueVault as an MCP server
Open your Claude Code MCP config at ~/.claude/claude_desktop_config.json. On Claude Desktop for macOS the path is ~/Library/Application Support/Claude/claude_desktop_config.json — on Linux it’s ~/.config/claude/claude_desktop_config.json. For other MCP-compatible IDEs, check their documentation.
Add:
{ "mcpServers": { "opaquevault": { "command": "ov", "args": ["mcp", "serve"] } }}Restart Claude Code after saving.
5. Set project context with .ov.yaml
In your project root, create .ov.yaml:
app: my-appenvironment: developmentThis tells OpaqueVault which app/environment to use when Claude calls vault tools in this directory. You don’t have to specify it on every call.
6. Unlock your vault when you start working
ov vault unlockThis starts the local ov mcp serve process and holds your KEK in memory for the session. You’ll need to unlock once per session (or configure auto-unlock for convenience).
7. Add OpaqueVault context to your CLAUDE.md
The more reliable way to direct Claude’s behavior isn’t per-session instructions — it’s your project’s CLAUDE.md, which Claude reads at every session start. Add:
## Secret managementThis project uses OpaqueVault for secrets.- Use vault_run to run commands that need credentials- Use vault_list_secrets to see what's available- Never read .env files for credentials — use vault_run insteadWith this in place, Claude applies the vault_run pattern automatically without you needing to repeat it each session. Per-session instructions (“use vault_run for this command”) work fine too, but CLAUDE.md is the durable version.
8. What happens if the vault isn’t unlocked
If ov mcp serve isn’t running or the session has expired, vault_run fails with a clear error:
Error: vault session not active — run 'ov vault unlock' to start a sessionClaude will see this error and tell you to unlock. It won’t fall back to reading .env or prompting you for credentials directly. The failure mode is explicit, not silent.
Your .env file is now either empty or contains only non-sensitive values like:
# .env — safe to have Claude read thisNODE_ENV=developmentLOG_LEVEL=debugPORT=3000# No secrets hereOrganizing secrets across environments
One of OpaqueVault’s strengths is the app/environment/name hierarchy. It maps directly to how real projects are structured:
my-app/├── development/│ ├── DATABASE_URL → local postgres│ ├── STRIPE_SECRET_KEY → Stripe test secret key│ └── OPENAI_API_KEY → dev key with spending limits├── staging/│ ├── DATABASE_URL → staging RDS│ ├── STRIPE_SECRET_KEY → Stripe test secret key│ └── OPENAI_API_KEY → staging key└── production/ ├── DATABASE_URL → production RDS ├── STRIPE_SECRET_KEY → Stripe live secret key └── OPENAI_API_KEY → production keyWhen you switch from development to staging work, update .ov.yaml:
app: my-appenvironment: stagingvault_run now injects staging secrets automatically.
Listing and auditing what you’ve stored
# List all secrets (names only, no values)ov secret list my-app/development
# Output:# my-app/development/DATABASE_URL# my-app/development/STRIPE_SECRET_KEY# my-app/development/OPENAI_API_KEY# my-app/development/SENDGRID_API_KEYClaude can also call vault_list_secrets to see what’s available. This returns names only — never values.
What about sharing secrets with teammates?
OpaqueVault is currently designed for individual developer workflows. Each developer has their own vault, encrypted with their own master password. The namespace structure (app/environment/name) is defined in .ov.yaml which lives in version control — so your team shares the structure automatically, but each developer manages their own credential values.
For distributing the actual secret values to teammates, use a separate secure channel — 1Password shared vaults, an encrypted note, or your organization’s existing secrets distribution process. Team vaults with shared KEK management are on the roadmap.
Migrating from .env
If you have an existing .env file, use ov secret import — it handles quoted values, export prefixes, comments, and multi-line values correctly:
# Preview what will be imported (no writes)ov secret import .env --dry-run
# Import with a namespace prefixov secret import .env --prefix my-app/development/
# Verify what landedov secret list my-app/developmentAfter import, remove the secret values from .env and leave only non-sensitive configuration (ports, log levels, feature flags).
Summary
| Don’t do this | Do this instead |
|---|---|
Store API keys in .env | Store in ov secret create app/env/NAME |
| Let Claude read your secrets | Configure OpaqueVault as MCP server |
| Run authenticated commands directly | Use vault_run for subprocess injection |
| One flat namespace | Use app/environment/name hierarchy |
OpaqueVault is a zero-knowledge MCP secret manager for Claude Code and other AI coding agents. Get started free →
Related: How to keep API keys secure with Claude Code · MCP Secret Manager — How OpaqueVault Works