← Back to Vault
· Updated

How to Keep API Keys Secure When Using Claude Code

Keep API keys out of readable project files and use scoped command execution to reduce exposure in AI coding workflows.

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

A useful approach is to keep plaintext credentials out of agent-readable files and provide narrowly scoped operations that use credentials without returning their values.

“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 exit code and run metadata
→ Key never entered context window

The local daemon decrypts selected credentials and injects them into the authorized child process. The MCP response omits stdout and stderr; output goes to the daemon terminal or agent log. The child process and local output sinks remain trusted, and another tool with access to them can create a separate exposure path.

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 exit code and run metadata

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 path: vault_run omits subprocess stdout and stderr from the MCP response. The interceptor provides additional pattern detection over other returned content; it is not a sandbox and can miss transformed credentials.

Protected data: The remote API stores encrypted secret values without their decryption keys. Account and operational metadata remain visible to the service.

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 by this response boundary: Other shell, filesystem, or network tools that can access credentials or secret-bearing logs. Restrict those paths and review commands before granting them credentials.

The audit trail

Every vault_run call generates an audit log entry. In the audit log, secret names are recorded 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.

The log is reachable through the API’s GET /v1/audit endpoint, which returns entries carrying HMAC’d resource references rather than plaintext names or IDs. There is no ov subcommand wrapping it yet — today it’s an API surface, not a CLI workflow.

Be precise about who this hides things from. The audit HMAC key is server-side: the API loads a root key from its own environment and derives a per-user subkey with HKDF-SHA256. It is not derived from your KEK. So the hashing defends against a leaked log — an attacker who exfiltrates the audit table alone sees timestamps and operation types but cannot map a hash back to a secret name. It does not defend against OpaqueVault, which holds the root key and can recompute the HMAC to determine which secret an entry refers to.

That is a deliberately narrower guarantee than the one covering secret values, and it’s worth keeping the two straight. Values are unreadable to the server by construction — the KEK never leaves your machine. Audit metadata is not: the server can see that a secret was accessed and, if it chooses to recompute, which one. If your threat model includes the server operator, secret values are still safe, but your access patterns are not the thing being protected here.

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.

Updated October 23, 2026: aligned OpaqueVault’s daemon and MCP response descriptions with the current tool contract. Local commands and output sinks remain part of the trust boundary.

Zero-knowledge secrets for AI agents

Keep credentials out of Claude's context window.

OpaqueVault encrypts secrets client-side and injects them into authorized subprocesses. MCP responses contain no stored secret values or subprocess output.

Get started free →← More from Vault