← All posts

How to Store Secrets for Claude Code (Without Putting Them in .env)

The standard .env approach exposes your API keys to Claude's context window. Here's the correct way to store and use secrets with Claude Code.

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 — not the secret value.

One important nuance: this architecture prevents secrets from appearing directly in Claude’s context, but it doesn’t prevent a subprocess from echoing secrets to its own stdout. If you run a command that dumps environment variables (like env or printenv), the output would contain your secrets — and that output would return to Claude. OpaqueVault’s interceptor scans subprocess output for known secret patterns before returning it, providing a second layer of protection. But the stronger guarantee is: don’t run commands that introspect the environment.

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

Terminal window
brew install opaquevault-brew/tap/ov

On Linux or without Homebrew:

Terminal window
curl -fsSL https://install.opaquevault.com | sh

If you prefer to verify before executing, download and inspect the script first:

Terminal window
curl -fsSL https://install.opaquevault.com -o install.sh
# Review install.sh, then:
sh install.sh

Or install from a pre-built binary release and verify the SHA-256 checksum listed on the release page:

Terminal window
# Download the checksum file and binary, then verify
sha256sum -c ov_<version>_checksums.txt

2. Initialize your vault

Terminal window
ov auth login

You’ll enter your email and master password. The master password 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

Create your app and store each secret:

Terminal window
# Create your app
ov app create my-app
ov app use my-app
# Database
ov secret set DATABASE_URL
# Stripe (secret key only — publishable keys are safe to put in source code)
ov secret set STRIPE_SECRET_KEY
# OpenAI (if your app calls it directly)
ov secret set OPENAI_API_KEY
# Any other service
ov secret set SENDGRID_API_KEY

Each 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

Add OpaqueVault to your Claude Code MCP config (~/.claude.json or a project-level .mcp.json):

{
"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-app
env: development

This 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. Start the MCP server

When you launch Claude Code with OpaqueVault configured, ov mcp serve starts automatically and prompts for your master password. The KEK is held in process memory, is not written to disk, and is zeroed when the session ends. OpaqueVault calls mlock to attempt to pin it in RAM and exclude it from core dumps — but mlock requires sufficient OS permissions; on systems where it fails, the KEK remains in normal unlocked memory. See the security model for full details.

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 management
This 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 instead

With 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 — restart ov mcp serve to start a session

Claude 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:

Terminal window
# .env — safe to have Claude read this
NODE_ENV=development
LOG_LEVEL=debug
PORT=3000
# No secrets here

Organizing secrets across environments

Secrets are scoped by app and environment. You set the active context with ov app use and ov env use, and all commands target it. Each environment has its own set of flat-named secrets:

my-app / development: DATABASE_URL, STRIPE_SECRET_KEY, OPENAI_API_KEY
my-app / staging: DATABASE_URL, STRIPE_SECRET_KEY, OPENAI_API_KEY
my-app / production: DATABASE_URL, STRIPE_SECRET_KEY, OPENAI_API_KEY

When you switch from development to staging work, update .ov.yaml:

app: my-app
env: staging

vault_run now injects staging secrets automatically.

Listing and auditing what you’ve stored

Terminal window
# List all secrets (names only, no values)
ov secret list
# Output:
# DATABASE_URL
# STRIPE_SECRET_KEY
# OPENAI_API_KEY
# SENDGRID_API_KEY

Claude 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 a single-developer tool. Each developer has their own vault encrypted with their own master password. The .ov.yaml config lives in version control so your team shares the app context automatically — but each developer provisions their own secret values.

That’s a real limitation for team workflows. For now, use your existing secure channel for the initial distribution of values: 1Password shared vaults, your organization’s secrets manager, or direct secure transfer. Team vaults with shared key 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:

Terminal window
# Preview what will be imported (no writes)
ov secret import .env --dry-run
# Import
ov secret import .env
# Verify what landed
ov secret list

After import, remove the secret values from .env and leave only non-sensitive configuration (ports, log levels, feature flags).

Summary

Don’t do thisDo this instead
Store API keys in .envStore with ov secret set NAME
Let Claude read your secretsConfigure OpaqueVault as MCP server
Run authenticated commands directlyUse vault_run for subprocess injection
Secrets scattered across filesCentralized vault scoped by app and environment

OpaqueVault is an end-to-end encrypted MCP secret manager for Claude Code and other AI coding agents — secrets are encrypted on your machine before leaving it, and the server stores only ciphertext it cannot decrypt. Get started free →

Related: How to keep API keys secure with Claude Code · MCP Secret Manager — How OpaqueVault Works


Corrections (April 2026):

  • Verified MCP configuration paths are correct for Claude Code (~/.claude.json user-level, .mcp.json project-level).
  • Added SHA-256 checksum verification command for install step.

Zero-knowledge secrets for AI agents

Keep credentials out of Claude's context window.

OpaqueVault encrypts secrets client-side and injects them into subprocesses — your AI agent never sees the plaintext value.

Get started free → ← More posts