← All posts

How to Keep API Keys Secure When Using Claude Code

Claude Code reads your project files, including .env. Here's the right way to handle API keys so they never enter the model's context window.

Claude Code is a powerful AI coding assistant — and by default, it has access to your project files. That includes .env. If your API keys are stored there, they’re in Claude’s context window every time you open a session.

This page covers the right way to handle API keys when using Claude Code: what the actual risks are, what doesn’t work, and the architecture that keeps credentials out of the model’s context permanently.

Disclosure: We built OpaqueVault to solve this problem. The architectural analysis below is accurate regardless of which tool you use, but the specific solution we recommend is our own product.

The .env problem

Most developers store API keys in .env files. It’s the standard pattern — dotenv loads them at runtime, .gitignore keeps them out of version control, and everything works fine in a traditional development workflow.

Claude Code breaks this assumption.

When Claude Code is active in your project, it reads files to understand your codebase. If you ask it to “add Stripe billing” or “debug this API call,” it will read your project files — including .env — to understand the context. Your STRIPE_SECRET_KEY ends up in the model’s context window, gets sent to Anthropic’s API as part of the conversation, and potentially lives in conversation history.

The .gitignore on your .env file protects you from version control exposure. It does nothing to protect you from AI context exposure.

What doesn’t work

Gitignoring .env — Prevents VCS exposure, not AI context exposure. Claude Code reads from the filesystem, not from git.

Using .env.local — Same problem. Local files are still local files. Claude can read them.

Telling Claude “don’t read .env” — Unreliable. You’d have to remember to say this every session. One forgotten instruction and the key is in context.

Rotating keys frequently — Good hygiene but doesn’t solve the structural problem. Rotated keys are still exposed in the same way if the underlying pattern doesn’t change.

Moving keys to OS keychain — Prevents filesystem exposure, but doesn’t address the MCP tool layer. If Claude calls a tool that reads from the keychain, the value can still surface.

The correct architecture: zero-knowledge MCP vault

The only reliable solution is to keep credentials out of the filesystem entirely and give Claude a way to use credentials without seeing them. This is exactly what a zero-knowledge MCP secret manager does.

“Zero-knowledge” is a specific cryptographic claim: the server cannot decrypt your secrets, ever. In OpaqueVault’s case, your master password never leaves your machine — it derives a Key Encryption Key (KEK) via Argon2id client-side. Each secret gets a unique Data Encryption Key (DEK) that is wrapped with the KEK and stored server-side as ciphertext. The server holds the wrapped DEK and the encrypted value. Without your KEK — which only you have — neither is decryptable. The server is a ciphertext store with no decryption endpoint.

With OpaqueVault configured as an MCP server in Claude Code, the credential flow changes fundamentally:

Before (unsafe):

API key in .env file
→ Claude reads .env
→ Key enters context window
→ Key transmitted to Anthropic API
→ Key potentially in conversation history

After (safe):

API key encrypted in OpaqueVault
→ Claude calls vault_run("your command here")
→ OpaqueVault decrypts key locally in memory
→ Key injected into subprocess environment
→ Claude receives command output only
→ Key never entered context window

The key is in exactly one place: the memory of the ov mcp serve process, for the duration of the subprocess execution. It never touches a file Claude can read.

Setting this up

Step 1: Remove keys from .env

Move your API keys out of .env and into OpaqueVault:

Terminal window
# Install
brew install opaquevault-brew/tap/ov
ov vault init
# Store each key
ov secret create my-project/development/STRIPE_SECRET_KEY
ov secret create my-project/development/OPENAI_API_KEY
ov secret create my-project/development/DATABASE_URL

Step 2: Configure OpaqueVault as an MCP server

Add to ~/.config/claude/claude_desktop_config.json:

{
"mcpServers": {
"opaquevault": {
"command": "ov",
"args": ["mcp", "serve"]
}
}
}

Step 3: Set your project context

Create .ov.yaml in your project root:

app: my-project
environment: development

Step 4: Update your workflow

Instead of Claude reading your .env and running commands with credentials in context, you now tell Claude to use vault_run:

"Run the Stripe test charge script using my vault secrets"
→ Claude calls vault_run("node scripts/test-charge.js")
→ STRIPE_SECRET_KEY injected into the Node process
→ Claude sees the output, not the key

Your .env file can now be empty or contain only non-sensitive configuration (feature flags, log levels, public API endpoints).

What about CI/CD?

Claude Code is a local development tool, but the same credentials often need to be available in CI. This raises a fair question: if writing secrets to a .env file is the problem, isn’t ov secret export just recreating it?

The difference is scope and lifetime. In a local dev workflow, a .env file is a persistent artifact that Claude reads in every session indefinitely. In CI, the export creates a file inside an ephemeral container that is destroyed after the run completes — no AI agent reads it, it never touches version control, and it’s gone when the job ends.

Terminal window
# In your CI pipeline — ephemeral container, no AI agent in the loop
ov secret export my-project/production > .env
# Run your build/test/deploy
# Container destroyed, .env gone with it

Combined with a short-lived session token stored as a CI secret (not a master password), this keeps the zero-knowledge property intact: the token has limited scope, the export is transient, and no persistent plaintext artifact survives the run. The pattern to avoid is a .env that persists across sessions and is readable by an AI agent — not a .env that lives for 30 seconds inside a locked-down container.

Threat model: what OpaqueVault protects against (and what it doesn’t)

It’s worth being explicit about the threat model:

Protected: Claude reading your .env or config files and seeing raw credential values. The vault removes the credential from the filesystem layer entirely.

Protected: vault_run output containing a credential. The interceptor scans all MCP responses before they reach Claude and blocks any high-entropy token that matches known secret patterns. If a subprocess echoes a key in its stdout, it gets blocked.

Protected: Server compromise. The remote server holds only ciphertext. Without your KEK — which never leaves your machine — the ciphertext is useless.

Not protected by architecture alone: A compromised MCP server on your machine. If an attacker has code-execution on your machine with your user permissions, they can read memory, intercept subprocesses, or exfiltrate your KEK directly. This is the same threat model as any local credential store (1Password, OS keychain). Defense-in-depth at the OS level — full disk encryption, application sandboxing, keeping your machine clean — applies here as with any other secret.

Not protected: Prompt injection attacks that convince Claude to run ov secret export and then read the output. This is why the vault has no get_secret tool and the interceptor is always active — the attack surface is minimized, but security-conscious users should also review what web content and untrusted inputs their Claude sessions are exposed to.

The audit trail

Every vault_run call generates an audit log entry. Secret names are stored as HMAC-SHA256 hashes rather than plaintext — which means the log records that a secret was accessed without the log itself becoming a credential discovery vector.

To query whether a specific secret was accessed, you hash its name with the same HMAC key and search the log:

Terminal window
ov audit query --secret STRIPE_SECRET_KEY
# Returns: timestamps of all accesses matching that secret's HMAC hash

The HMAC key is derived from your KEK, so only you can map hashes back to secret names. The audit log is genuinely private — an attacker who obtained the raw log would see access timestamps and operation types, but not which secrets were accessed.

Quick reference: Claude Code API key safety checklist

Vendor-neutral steps (do these regardless of which tool you use):

  • Rotate any credentials that may have been in .env files Claude has read
  • Audit your claude_desktop_config.json and any .mcp.json files for embedded credentials
  • Search your git history: git log -S "sk_live" --all — keys committed historically are still at risk
  • Review what files are in Claude’s project context and remove any that contain secrets

OpaqueVault-specific steps:

  • No API keys in .env files in Claude Code project directories
  • No API keys in MCP config files (claude_desktop_config.json)
  • OpaqueVault configured as MCP server
  • .ov.yaml in project root sets app/environment context
  • Claude instructed to use vault_run for authenticated operations
  • CI uses session tokens, not raw keys

The .env pattern predates AI-assisted development by a decade. It solved a real problem — keeping secrets out of version control — and it’s still the right answer for that threat. What it was never designed for is an AI agent with filesystem read access running in the same environment. Updating your credential hygiene for the AI development era isn’t paranoia; it’s recognizing that the threat model changed.

OpaqueVault keeps API keys out of Claude’s context window permanently. Get started free →

Related: Why your AI coding assistant is a secret leak waiting to happen · MCP Secret Manager — How OpaqueVault Works

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