On this page
Don't paste API keys into your MCP config file. Put a reference there
instead: ${VAR} in Claude Code, ${env:NAME} in Cursor, or a hidden
${input:...} prompt in VS Code. Keep the real key outside the project, and
prefer servers that sign in with OAuth and scoped tokens.
This came up again and again on Reddit in the last two weeks. One r/mcp post started "I got tired of putting API keys into Claude Code / Cursor configs" (thread). Another was titled "My agent could read every API key I had" (thread). And an r/ClaudeCode user asked Claude Code to find every secret on their laptop, and it found plenty (thread).
Why this happens
Most setup guides show the quickest path: copy a JSON block and paste your key into it. That works, but it leaves the key in plain text in a file that:
- often gets committed. In Claude Code, project-scoped servers live in
.mcp.jsonin the project root, and Anthropic's docs say to check that file into version control so your team gets the same servers. - sits next to your code. A coding agent with file tools can open
.cursor/mcp.jsonor.vscode/mcp.jsonlike any other file. - gets copied around. Configs are shared in chats, gists and screenshots.
Claude Code's claude mcp add --env KEY=value saves the value into the config
too: local and user scopes are stored in ~/.claude.json.
Step 1: Replace the key with a reference
All three clients can read a value from somewhere else when the server starts. The snippets below are copied from each vendor's docs.
Claude Code (.mcp.json). You can reference an environment variable in
command, args, env, url or headers, as ${VAR} or
${VAR:-default}
(docs):
{
"mcpServers": {
"api-server": {
"type": "http",
"url": "${API_BASE_URL:-https://api.example.com}/mcp",
"headers": {
"Authorization": "Bearer ${API_KEY}"
}
}
}
}
If the variable isn't set, the config still loads, claude mcp list shows a
missing-variable warning for that server, and the literal ${VAR} text is
sent as-is. So check that list after setup.
Cursor (.cursor/mcp.json). Cursor resolves ${env:NAME} in command,
args, env, url and headers
(docs):
{
"mcpServers": {
"remote-server": {
"url": "https://api.example.com/mcp",
"headers": {
"Authorization": "Bearer ${env:MY_SERVICE_TOKEN}"
}
}
}
}
For local (stdio) servers, Cursor also supports an envFile option that loads
variables from a file such as .env. Remote servers don't support envFile.
VS Code (.vscode/mcp.json). Use an input variable. VS Code asks for the
value the first time the server starts and then stores it securely for later
use (docs):
{
"inputs": [
{
"type": "promptString",
"id": "perplexity-key",
"description": "Perplexity API Key",
"password": true
}
],
"servers": {
"perplexity": {
"type": "stdio",
"command": "npx",
"args": ["-y", "server-perplexity-ask"],
"env": {
"PERPLEXITY_API_KEY": "${input:perplexity-key}"
}
}
}
}
"password": true hides what you type. VS Code also supports envFile for
stdio servers.
Step 2: Keep the real value out of the repo
- Set the variable in your shell profile, your OS keychain tooling, or a secrets manager. Don't put it in a file inside the project.
- If you do use an
.envfile withenvFile, add it to.gitignore. VS Code's docs say it plainly: keep files containing credentials out of source control. - Search your repo history once:
git log -p -S "sk_"(swap in your key's prefix). If a key was ever committed, treat it as leaked.
Step 3: Make the key worth less
A reference keeps the key out of files. It does not hide it from an agent that can run shell commands, because environment variables are readable by the processes you start. So also limit what the key can do:
- Use a restricted key. Most vendors let you create keys with fewer permissions or read-only access. Cursor's docs give the same advice.
- Prefer OAuth. Hosted servers that sign you in through the browser don't need a key on disk at all.
- Rotate keys you've pasted anywhere. Revoke and reissue in the vendor's dashboard. It takes a minute.
Safety note: what the trust grade tells you
Authentication is the most heavily weighted of the four checks in our trust grade. Servers that offer OAuth with real scopes or a read-only mode score highest, because a leaked token can do less. For example:
- Buildkite (A) has a read-only endpoint that issues a read-only token.
- Cloudflare (A) lets you pick a read-only grant at sign-in.
- Slack (A) uses per-tool scopes and needs admin approval.
Watch for keys that act for your whole account. Firecrawl's OAuth or API key unlocks everything on the account, and the first setup Tavily's docs show puts the API key in the server URL, where it can end up in logs. Both offer OAuth, which keeps the key off your disk. Use it where your client supports it.
For the full pre-install check, see is this MCP server safe to install?.
Frequently asked questions
Is it safe to commit .mcp.json?
Yes, if it holds only references like ${API_KEY} and no real values. That is
what Claude Code's variable expansion is for: the team shares the file, and each
person sets their own key.
Does using an environment variable hide the key from the agent?
No. It keeps the key out of files you share or commit. An agent that can run shell commands can still read your environment. Limit the key's permissions and keep tool approval on for shell access.
Where does claude mcp add --env store the key?
In the config for that scope. Local and user scopes are stored in
~/.claude.json and project scope in .mcp.json. Use a ${VAR} reference
in .mcp.json instead if you want the value to stay out of files.
My key ended up in a public repo. What now?
Revoke it in the vendor's dashboard first, then remove it from the file. Deleting the file isn't enough, because the key stays in git history and may already have been copied.
What about tools that inject credentials for the agent?
Some r/mcp posts describe local brokers that keep keys in the OS keychain and let the agent request actions instead of holding tokens (example). We haven't reviewed these. If you try one, it is another MCP server with access to all your keys, so check it with the same care.