MCP Secrets Exposure

Your MCP config is a plaintext secrets file.

Every official MCP quickstart teaches the same pattern: paste your API key into the config JSON. It works instantly. It also stores live credentials in plaintext on disk, readable by every process on the machine and one accidental git add . away from being public. Keelix finds these secrets and flags them — without a vault, without a SaaS platform, and without the secrets ever leaving your box.

Where secrets end up in MCP config

Common pattern — plaintext secret in MCP config
// ~/.claude/claude_desktop_config.json
{
  "mcpServers": {
    "my-database-tool": {
      "command": "npx",
      "args": ["-y", "some-mcp-server"],
      "env": {
        "DATABASE_URL": "postgresql://user:PASSWORD@host/db",
        "OPENAI_API_KEY": "sk-proj-..."
      }
    }
  }
}
Keelix flags plaintext credentials as a HIGH severity finding

How widespread is this?

The attack surfaces

Plaintext secrets in MCP config create risk across three distinct surfaces — each independent, any one sufficient.

01 · VERSION CONTROLHIGH

Version control leakage

A single git add . in a directory containing a config file puts live credentials into history. Public repos are scanned by automated tools within minutes of a push — GitGuardian, Trufflehog, and GitHub's own push protection all watch for this. Rotating the key is mandatory; git history scrubbing is required.

02 · PROMPT INJECTIONHIGH

LLM context injection

The agent that reads the config file has the file's contents in context — including the credentials. A malicious website, document, or MCP tool output embedding an exfiltration instruction can direct the agent to transmit those values. This attack is especially dangerous when combined with agent auto-approval.

03 · SUPPLY CHAINMEDIUM

Rogue MCP server read

Any MCP server running on the box executes as the user. A server with a supply-chain compromise, a rogue package installed via npm/pip, or a server whose tool descriptions contain injection patterns can read the config file and exfiltrate its contents through a legitimate-looking tool call.

Remediation: fix the config, not just the symptom

OPTION 1 · ENVIRONMENT VARIABLE REFERENCE

Use the ${env:VAR_NAME} syntax to reference an environment variable already set in your shell profile (~/.zshrc, ~/.bashrc). The config file no longer contains the value itself — only a reference to it. This pattern keeps secrets out of version control while requiring no additional tooling.

"env": {
  "OPENAI_API_KEY": "${env:OPENAI_API_KEY}"
}
OPTION 2 · OAUTH FOR REMOTE SERVERS

Remote MCP servers that implement OAuth 2.1 handle authentication through a browser flow — no token is ever written to a config file. This is the strongest option for server-to-server integrations. Not all MCP servers support it yet, but adoption is growing as the MCP authorization spec matures.

IF ALREADY COMMITTED TO GIT

Rotate the secret immediately — assume it has been indexed. Do not delete the commit and push a new one; the credential is in history. Use git-filter-repo to rewrite history, then force-push and notify all collaborators to re-clone. Private repos are not safe to skip this step — tokens can be harvested via repository access, compromised collaborator accounts, or leaked deploy keys.

What Keelix checks — and what it does not do

Secrets scan — config paths Keelix reads
~/.claude/claude_desktop_config.json — global Claude Desktop config
~/.claude/settings.json — Claude Code global settings
.claude/settings.json — project-level Claude Code settings
.mcp.json — repo-root MCP config
~/.cursor/mcp.json — Cursor global MCP config
~/.codeium/windsurf/mcp_config.json — Windsurf MCP config
Secrets redacted at scan boundary — values never written to output

Frequently asked

Where does Claude Code store secrets?
Claude Code reads MCP server configuration from several files: ~/.claude/claude_desktop_config.json (global), .claude/settings.json (project-level), and .mcp.json at the repository root. API keys, tokens, and database connection strings are commonly passed as environment variables inside the mcpServers JSON object — which means they sit in plaintext on disk, readable by any process running as the same user. Keelix scans all of these paths when it runs.
Why do MCP config files contain plaintext secrets?
Most official MCP setup guides recommend passing API keys directly as environment variables inside the server config JSON. The pattern works immediately and is simple to follow. The problem is that this normalizes storing live credentials in files that users frequently commit to version control, sync via dotfile managers, or share as project scaffolding — turning a convenience shortcut into a widespread credential exposure pattern.
What is the blast radius if MCP config secrets leak?
It depends on which secrets are exposed, but the worst case is substantial: Anthropic API keys, database passwords, cloud provider credentials, email service tokens, payment API keys, and OAuth client secrets all appear in real MCP config files. A leaked Anthropic key can generate unlimited model calls billed to your account. A leaked database password gives read/write access to production data. Secrets committed to a public repo are indexed by tools like GitHub search and credential scanners within minutes of the push.
How do I scan for plaintext secrets in MCP config?
Run `keelix scan` on the box. Keelix reads every MCP config path used by Claude Code, Cursor, Windsurf, and other agents it discovers on the host, flags any plaintext API keys or tokens it finds, and redacts them before the report is written — the actual secret never appears in output or telemetry. Each finding includes the config file path, the key type (inferred from format), the remediation step, and its SOC 2 / CIS control mapping.
Does Keelix store or transmit the secrets it finds?
No. Keelix runs entirely on your machine. When it discovers a plaintext secret, it redacts the value at the scan boundary — the report records that a finding exists, the config path, and the inferred credential type, but never the credential value itself. No data leaves your box. This is the same local-first, zero-telemetry design that applies to every Keelix scan.
How do I fix plaintext secrets in MCP config?
The safest approach for local MCP clients (Claude Code, Cursor) is to use the ${env:VAR_NAME} syntax in the config file to reference environment variables set in your shell profile, rather than hardcoding the value. For remote MCP servers, prefer OAuth 2.1 flows that remove token management from the client entirely. For secrets already committed to version control, rotate them immediately — assume they have been scanned and indexed — then scrub the history using a tool like git-filter-repo.

Find out which secrets are in your MCP config right now.

Free, local-first, Apache-2.0. Secrets redacted at the boundary — nothing leaves your box.

operator@host — keelix
# install — free & open source (Apache-2.0)
$ curl -fsSL https://keelix.dev/install.sh | sh
$ keelix scan