Answers · MCP security
How do I keep API keys and tokens out of MCP client config files like mcp.json and claude_desktop_config.json?
Last updated
TL;DR: Put a reference in the config file and keep the secret somewhere built to hold it. MCP client config files sit in known locations on disk (the MCP docs list claude_desktop_config.json under ~/Library/Application Support/Claude on macOS, %APPDATA%\Claude on Windows and ~/.config/Claude on Linux), get copied into dotfile repos and project folders, and VS Code's docs say plainly that a personal configuration file is not a secret store. Each major client now supports references: Claude Code expands ${VAR} and ${VAR:-default} in .mcp.json, Cursor resolves ${env:NAME} in mcp.json, VS Code prompts once for ${input:...} variables (password: true) and stores the value securely, and Claude Desktop extensions (MCPB) mark user_config fields sensitive so they are masked and stored securely. For clients with only a literal env block, make the command a launcher such as 1Password's op run that resolves op:// references at start time. Then shrink what a leak is worth: one narrowly scoped, short-lived token per server, a secret scanner on every commit, and rotation if a key was ever written in plain text.
What should I check, step by step?
1. Find every literal secret today: grep your client configs (~/.cursor/mcp.json, .cursor/mcp.json, .mcp.json, .vscode/mcp.json, ~/.claude.json and claude_desktop_config.json) for strings that look like keys, and run a scanner such as gitleaks over the repos and dotfile folders that contain them.
- Claude Code: replace the value with "${GITHUB_TOKEN}" in .mcp.json; ${VAR:-default} works for non-secret settings like URLs. Its docs note that its own and cloud-provider credentials (ANTHROPIC_API_KEY, AWS_BEARER_TOKEN_BEDROCK, NPM_TOKEN and others) read as empty in a remote server's url and headers, so give each server its own variable name.
- Cursor: use "${env:NAME}" in env, headers, args or url. Its docs offer envFile for stdio servers; if you use it, keep that file out of the repo and out of the project folder the agent can read.
- VS Code: declare an inputs entry with type promptString and password: true and reference it as ${input:id}; VS Code prompts on first start and stores the value securely. Note its docs say input variables aren't supported in portable .mcp.json files.
- Claude Desktop: prefer an MCPB extension whose manifest declares the key as user_config with sensitive: true, passed to the server as ${user_config.api_key} in env. For a hand-written server entry, point command at a launcher (op run --env-file with op:// references, or a script that reads your OS keychain) so the JSON never holds the value.
- Remote servers: use the client's OAuth sign-in instead of a static bearer header where the server supports it. The MCP authorization spec is for HTTP transports; for stdio it says implementations should retrieve credentials from the environment, which is why the reference-plus-launcher pattern matters locally.
- Shrink the blast radius: one token per server with the smallest scope (a fine-grained token for one repo, read-only where possible), with an expiry. Never reuse a personal account-wide token across servers.
- Block regressions: gitleaks as a pre-commit hook and in CI, and GitHub secret scanning push protection on repos that hold configs. Keep .env and envFile paths in .gitignore.
- If a key ever sat in plain text in a config, rotate it. Removing it from the file doesn't remove it from git history, backups, synced dotfiles or anything a server already read.
Why do MCP client config files like mcp.json leak API keys?
They are predictable files. The MCP Local Server Security guide calls client configuration files a known, predictable location on disk and says to keep long-lived secrets out of plaintext config, preferring the OS secret store (macOS Keychain, Windows Credential Manager, freedesktop Secret Service) where your client can reference it.
- They get shared. Project-scoped files such as .mcp.json, .cursor/mcp.json and .vscode/mcp.json are meant to be committed so a team gets the same servers, which is exactly how a pasted token ends up in git. Setup guides that show "API_KEY": "<your key>" encourage the paste.
- They are readable by the things you are trying to limit. A coding agent or a local MCP server running as your user can read the config file as easily as you can, unless a sandbox denies it.
How do Claude Code, Cursor, VS Code and Claude Desktop reference secrets in MCP config instead of storing them?
Claude Code: ${VAR} and ${VAR:-default} in a server's command, args, env, url or headers. /mcp and claude mcp list show the reference by name rather than the resolved value. For remote servers that need short-lived tokens, headersHelper runs a command at each connection and merges its JSON output into the request headers, so no token is stored in the file.
- Cursor: ${env:NAME}, plus ${userHome} and ${workspaceFolder}, resolved in command, args, env, url and headers; OAuth client secrets in the auth block can also be ${env:...} references.
- VS Code: ${input:id} backed by an inputs entry (promptString with password: true) prompts once and stores the value securely; envFile is supported in the VS Code format. Claude Desktop extensions: sensitive user_config values are masked at entry and stored securely, then substituted as ${user_config.KEY}.
How do short-lived, scoped credentials replace static API keys in MCP server config?
A reference moves the secret out of the file; it doesn't make the secret less valuable. The MCP guide's next step is short-lived credentials issued at launch time by a secrets manager or broker, and rotating anything a server held when you remove it.
- 1Password's op run loads op:// secret references and runs the command in a subprocess with the secrets as environment variables only for the life of that process, so the launcher pattern works for any client that can run a command.
- Where you control the API the server calls, issue the server a credential bound to its own key and configuration with a TTL in hours, and have the API verify and be able to revoke it per server, instead of trusting a bearer string copied into JSON.
How do I ask Rightshold about this (free, no key)?
Add the MCP server https://www.rightshold.com/mcp to Claude, Cursor, ChatGPT, Grok or VS Code, then ask: “Instead of pasting a long-lived API key into my MCP config, issue my local reporting MCP server a credential that expires in 8 hours and covers only reading reports, so my API can verify it and I can revoke it on its own.”
{
"tool": "issue_agent_credential",
"arguments": {
"agent": "mcp-server:reports-reader",
"public_key_pem": "<Ed25519 PEM generated at launch, kept in memory>",
"config": {
"model_id": "provider/model@version",
"system_prompt_sha256": "<hex>",
"tools": [
"list_reports",
"get_report"
],
"code_version": "v1.4.2"
},
"scope": [
"reports:read"
],
"ttl_hours": 8,
"signature": "<server key signs the CredentialRequest>"
}
}- Needs your API key, held by the launcher rather than written into mcp.json. verify_agent_credential is free and keyless: your API checks the credential's signature, expiry, revocation and that its scope covers reports:read before answering.
- revoke_agent_credential ends this one server's credential immediately (it appears on the signed revocation list) without rotating keys for anything else.
- This helps only for APIs you control or front with a gateway. Third-party APIs still need their own scoped token, kept in a secret store and referenced as above.
- Rightshold does not read your config files, store your third-party secrets or scan your repositories; reference syntax, secret stores and scanners live in your client, OS and CI.
Rightshold issues, verifies and revokes agent credentials you request. It is not a secrets manager, does not hold your third-party API keys and does not scan your files or repositories. Not legal advice.
What are the sources?
- MCP docs: Local Server Security (Isolate Credentials)
- MCP docs: Connect to local MCP servers (claude_desktop_config.json locations)
- MCP specification: Authorization (stdio retrieves credentials from the environment)
- Claude Code docs: MCP (environment variable expansion in .mcp.json, headersHelper)
- Cursor docs: MCP (config interpolation, envFile)
- VS Code docs: MCP configuration reference (input variables, envFile)
- MCPB manifest spec (user_config sensitive)
- 1Password CLI: Load secrets into the environment (op run)
- gitleaks
- GitHub Docs: About push protection
- OWASP Secrets Management Cheat Sheet
- Agent credentials on Rightshold
Related
- How to keep .env secrets and API keys away from AI coding agents
- How to give an AI agent a scoped, expiring credential instead of a long-lived API key
- How to give an MCP server least-privilege access to GitHub
- How to rotate API keys and tokens after a malicious VS Code or Cursor extension
Not legal advice. JSON · Markdown · All answers
Get a free API key in one call
No card and no email needed. 100 free credits; the license and policy checks stay free without a key.
curl -s -X POST https://www.rightshold.com/v1/accounts/signup -H "Content-Type: application/json" -d '{"accept_terms":true}'
export RIGHTSHOLD_API_KEY=rs_... # the api_key from the reply (shown once)
curl -s https://www.rightshold.com/v1/licenses/check -H "Authorization: Bearer $RIGHTSHOLD_API_KEY" \
-H "Content-Type: application/json" -d '{"license":"MIT","intended_use":{"commercial_use":true}}'
