Not all MCP servers need secrets. Local tools (filesystem, database) might not need any credentials. Others use OAuth — Claude Code handles the auth flow and you never touch API keys.
But if your MCP server needs an API key or token, here’s how to handle it without exposing secrets.
The Risks
Before choosing an approach, understand what you’re actually protecting against:
1. Accidental Git Commits
One git add . and your API keys are on GitHub. If your repo is public, your secrets are immediately exposed.
2. Sharing Configs
You paste your .mcp.json in a blog post, Stack Overflow answer, or Slack message. If secrets are hardcoded, they’re now public.
3. Team Onboarding
New team member needs to set up MCP servers. If secrets are in config files, you’re emailing API keys around or committing them to a private repo that might become public.
4. Data Sent to Anthropic
When Claude reads your files, that content is sent to Anthropic’s servers for processing. You may not want your production API keys on a third party’s servers.
5. Prompt Injection Attacks
A malicious repository or webpage could contain hidden instructions that trick Claude into revealing secrets in its output. If secrets are in Claude’s context, they’re vulnerable.
6. Accidental Disclosure in Output
Claude might “helpfully” include your API key in a code suggestion, debug output, or commit message.
7. Echo/Print Tricks
Even with file read denied, Claude could run echo $API_KEY or printenv to access environment variables. File permissions don’t protect against this.
8. Unlocked Computer
You step away from your desk. Someone (or something) accesses your machine. Plaintext secrets in config files are immediately readable. Keychain actually helps here — it requires authentication to access.
9. Shared Machine Access
Multiple people or processes share your machine. Plaintext files are readable by anyone with file access.
The Standard Approach (And Why It’s Risky)
Hardcoding secrets in .mcp.json:
{
"mcpServers": {
"cloudflare": {
"command": "node",
"args": ["server.js"],
"env": {
"API_TOKEN": "sk-abc123-actual-secret"
}
}
}
}
This fails on almost every risk above.
The Solution: Environment Variable Expansion
Claude Code supports environment variable expansion in .mcp.json:
{
"mcpServers": {
"cloudflare": {
"command": "node",
"args": ["server.js"],
"env": {
"API_TOKEN": "${CLOUDFLARE_API_TOKEN}"
}
}
}
}
Syntax: ${VAR} or ${VAR:-default}
Works in: command, args, env, url, headers
Now your .mcp.json is safe to commit, share, and let Claude read. But where does the actual secret live?
Where to Store the Actual Secret
Option 1: .env File (Simple)
# In .env (add to .gitignore!)
CLOUDFLARE_API_TOKEN=your-actual-token-here
Protects against: Git commits (if gitignored), sharing configs
Doesn’t protect against: Claude reading it, prompt injection, unlocked computer
Warning: Claude auto-loads .env files without telling you.
Option 2: .zshrc (Outside Project)
# In ~/.zshrc
export CLOUDFLARE_API_TOKEN="your-actual-token-here"
Protects against: Git commits, sharing configs
Doesn’t protect against: Claude reading it, unlocked computer
Option 3: Doppler (Teams)
Doppler stores secrets in the cloud with native MCP integration:
{
"mcpServers": {
"cloudflare": {
"command": "doppler",
"args": ["run", "--project", "my-project", "--config", "dev", "--", "node", "server.js"]
}
}
}
Protects against: Git commits, sharing, team onboarding, data to Anthropic, prompt injection, unlocked computer
Doesn’t protect against: Echo tricks (secret is in env at runtime)
Good for teams: Access controls, audit trails, auto-rotation, free tier
Option 4: macOS Keychain
Keychain is an encrypted database, not a file. Requires authentication to access.
Store:
security add-generic-password -a "cloudflare" -s "api-token" -w "your-token"
Use in MCP:
{
"mcpServers": {
"cloudflare": {
"command": "/bin/sh",
"args": ["-c", "CLOUDFLARE_API_TOKEN=$(security find-generic-password -a cloudflare -s api-token -w) exec node server.js"]
}
}
}
Protects against: Git commits, sharing, data to Anthropic, prompt injection, unlocked computer (requires auth)
Doesn’t protect against: Echo tricks
Comparison
| Approach | Git | Share config | Team onboard | To Anthropic | Unlocked Mac |
|---|---|---|---|---|---|
| Hardcoded | ❌ | ❌ | ❌ | ❌ | ❌ |
| .env (gitignored) | ✅ | ✅ | ❌ | ❌ | ❌ |
| .zshrc | ✅ | ✅ | ❌ | ❌ | ❌ |
| Doppler | ✅ | ✅ | ✅ | ✅ | ✅ |
| Keychain | ✅ | ✅ | ❌ | ✅ | ✅ |
Which Should You Use?
Just want safe git commits? .env + .gitignore
Working alone, want real security? Keychain (protects even if you leave your Mac unlocked)
Working in a team? Doppler (handles sharing, onboarding, access control)
Other Secrets Managers
Same shell-wrapper pattern works with any CLI:
1Password: op read 'op://Vault/Item/token'
Bitwarden: bw get password my-api-token
AWS: aws secretsmanager get-secret-value --secret-id my-secret --query SecretString --output text
GCP: gcloud secrets versions access latest --secret=my-secret
Azure: az keyvault secret show --vault-name my-vault --name my-secret --query value -o tsv
Vault: vault kv get -field=token secret/mcp
Keychain Tips
Update: security add-generic-password -a "cloudflare" -s "api-token" -w "new-token" -U
Skip dialogs: security add-generic-password -a "cloudflare" -s "api-token" -w "token" -T /usr/bin/security
Performance: ~30ms at startup