{
  "question": "How do I keep API keys and tokens out of MCP client config files like mcp.json and claude_desktop_config.json?",
  "slug": "how-to-keep-api-keys-and-tokens-out-of-mcp-json-and-claude-desktop-config-files",
  "url": "https://www.rightshold.com/answers/how-to-keep-api-keys-and-tokens-out-of-mcp-json-and-claude-desktop-config-files",
  "short_answer": "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.",
  "checklist": [
    "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."
  ],
  "sections": [
    {
      "heading": "Why MCP config files leak keys",
      "points": [
        "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."
      ]
    },
    {
      "heading": "Reference syntax by client",
      "points": [
        "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}."
      ]
    },
    {
      "heading": "Short-lived, scoped credentials instead of static keys",
      "points": [
        "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."
      ]
    }
  ],
  "checks": [],
  "ask_rightshold": {
    "start": "https://www.rightshold.com/start?ref=seo",
    "mcp_endpoint": "https://www.rightshold.com/mcp",
    "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.",
    "mcp_call": {
      "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>"
      }
    },
    "also": [
      "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."
    ],
    "scope": "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."
  },
  "sources": [
    {
      "title": "MCP docs: Local Server Security (Isolate Credentials)",
      "url": "https://modelcontextprotocol.io/docs/tutorials/security/local-server-security"
    },
    {
      "title": "MCP docs: Connect to local MCP servers (claude_desktop_config.json locations)",
      "url": "https://modelcontextprotocol.io/docs/develop/connect-local-servers"
    },
    {
      "title": "MCP specification: Authorization (stdio retrieves credentials from the environment)",
      "url": "https://modelcontextprotocol.io/specification/draft/basic/authorization"
    },
    {
      "title": "Claude Code docs: MCP (environment variable expansion in .mcp.json, headersHelper)",
      "url": "https://code.claude.com/docs/en/mcp"
    },
    {
      "title": "Cursor docs: MCP (config interpolation, envFile)",
      "url": "https://cursor.com/docs/context/mcp"
    },
    {
      "title": "VS Code docs: MCP configuration reference (input variables, envFile)",
      "url": "https://code.visualstudio.com/docs/copilot/reference/mcp-configuration"
    },
    {
      "title": "MCPB manifest spec (user_config sensitive)",
      "url": "https://github.com/modelcontextprotocol/mcpb/blob/main/MANIFEST.md"
    },
    {
      "title": "1Password CLI: Load secrets into the environment (op run)",
      "url": "https://developer.1password.com/docs/cli/secrets-environment-variables/"
    },
    {
      "title": "gitleaks",
      "url": "https://github.com/gitleaks/gitleaks"
    },
    {
      "title": "GitHub Docs: About push protection",
      "url": "https://docs.github.com/en/code-security/secret-scanning/introduction/about-push-protection"
    },
    {
      "title": "OWASP Secrets Management Cheat Sheet",
      "url": "https://cheatsheetseries.owasp.org/cheatsheets/Secrets_Management_Cheat_Sheet.html"
    },
    {
      "title": "Agent credentials on Rightshold",
      "url": "https://www.rightshold.com/agent-credentials"
    }
  ],
  "related": [
    {
      "title": "How to keep .env secrets and API keys away from AI coding agents",
      "url": "https://www.rightshold.com/answers/how-to-keep-env-secrets-and-api-keys-away-from-ai-coding-agents"
    },
    {
      "title": "How to give an AI agent a scoped, expiring credential instead of a long-lived API key",
      "url": "https://www.rightshold.com/answers/how-to-give-an-ai-agent-a-scoped-expiring-credential-instead-of-a-long-lived-api-key"
    },
    {
      "title": "How to give an MCP server least-privilege access to GitHub",
      "url": "https://www.rightshold.com/answers/how-to-give-an-mcp-server-least-privilege-access-to-github"
    },
    {
      "title": "How to rotate API keys and tokens after a malicious VS Code or Cursor extension",
      "url": "https://www.rightshold.com/answers/how-to-rotate-api-keys-and-tokens-after-a-malicious-vs-code-or-cursor-extension"
    }
  ],
  "computed_from": "Curated guidance; license verdicts computed from Rightshold's license profiles at render time. Not legal advice.",
  "updated": "2026-11-07"
}