← All posts

How to Keep API Keys Secure When Using Claude Code

Claude Code can read 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 can end up in Claude’s context window whenever Claude reads the file on demand — for example, when a shell command sources it or when you instruct Claude to read it directly.

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 — Better than .env — Keychain encrypts at rest and requires per-app authorization, so Claude Code can’t silently read values from it. But the common pattern of pulling secrets out via the security CLI and exporting them into your shell environment re-exposes them. See Apple Keychain Is Not Enough for the full analysis.

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. Claude does see the command’s output — so if that output contains values derived from the secret (signed tokens, account identifiers), those are visible. The raw credential itself stays out of context.

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 auth login
# Create your app and store each key
ov app create my-project
ov app use my-project
ov secret set STRIPE_SECRET_KEY
ov secret set OPENAI_API_KEY
ov secret set DATABASE_URL

Step 2: Configure OpaqueVault as an MCP server

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

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

Step 3: Set your project context

Create .ov.yaml in your project root:

app: my-project

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. In CI, ov run with a machine key injects secrets into the subprocess directly — no .env file written, no persistent artifact:

Terminal window
# In your CI pipeline — ephemeral container, no AI agent in the loop
ov run --machine-key /run/secrets/machine.kek --app my-project -- ./deploy.sh
# Secrets injected into subprocess environment only
# Container destroyed after the run

Combined with a scoped machine key stored as a CI secret (not a master password), this keeps the zero-knowledge property intact: the key has limited scope, the injection is transient, and no plaintext artifact survives the run. See Machine Keys for the full CI setup.

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 (defense-in-depth): vault_run output containing a credential. The interceptor scans all MCP responses before they reach Claude and blocks tokens that match known secret patterns (provider prefixes, high-entropy strings, connection strings). This is a detection layer, not a prevention guarantee — pattern-based scanning has known false-negative rates, and a secret in an unrecognized format or split across multiple lines could pass through. The primary protection is that secrets are injected into the subprocess environment, not passed through stdout.

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 MCP config files (~/.claude.json for user-level, .mcp.json for project-level) 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.json, .mcp.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


Corrections (April 2026):

  • Corrected MCP configuration path: Claude Code uses ~/.claude.json (user-level) and .mcp.json (project-level), not ~/.config/claude/claude_desktop_config.json (which is Claude Desktop). (Claude Code docs)
  • Clarified that Claude Code reads .env files on demand, not automatically on every session.
  • Added qualification that the interceptor’s pattern-based scanning is a detection layer with known false-negative rates, not a prevention guarantee.

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