Rightshold™ Sign up

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.

  1. 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.
  2. 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.
  3. 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.
  4. 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.
  5. 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.
  6. 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.
  7. 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.
  8. 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.

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.

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.

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.”

One-click setup

{
  "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>"
  }
}

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?

Related

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}}'