Security: undeclared HTTP MCP transport persisted with credentials in global .credentials.json, presented as a project MCP, survives config removal
Summary
Claude Code persisted an HTTP MCP server transport ("forge") that the user
never declared in any project or user .mcp.json, stored it together with
credential material in the global file ~/.claude/.credentials.json,
surfaced it to the model/UI as if it were a project MCP, and retained
the entry across full restarts even after the user removed the corresponding
MCP configuration. Sensitive material (OAuth tokens, an API key, a
credential envelope) is stored in plaintext in that global file.
From the user's perspective this is indistinguishable from malware: an
undeclared channel that ships user credentials to a remote endpoint on every
startup, outside the user's declared configuration, and which the user cannot
remove by editing their config.
Environment
- Claude Code CLI 2.1.70 (also reproduced via the VS Code integration)
- Platform: Windows (win32), Node v22.17.0
- Project
.mcp.jsondeclares ONLY a local stdio server. No HTTP MCP
server is declared by the user anywhere.
Declared vs. actually loaded
Declared (project .mcp.json and .vscode/mcp.json):
- One server,
*_studio,type: stdio, a local command. No HTTP server.
Actually loaded at startup (CLI debug log):
[DEBUG] MCP server "forge": Initializing HTTP transport to https://<REDACTED-HOST>/mcp/
[DEBUG] MCP server "forge": HTTP transport options:
{"url":"https://<REDACTED-HOST>/mcp/",
"headers":{"User-Agent":"claude-code/2.1.70 (cli)",
"X-API-Key":"<REDACTED-API-KEY>"},
"hasAuthProvider":true}
[DEBUG] MCP server "forge": Successfully connected to http server
The HTTP "forge" server is not present in:
- the project
.mcp.json(stdio only), .vscode/mcp.json(stdio only),- the
mcpServersblock of~/.claude.json(empty / no forge entry), - any per-project
mcpServersblock in~/.claude.json.
Yet it loads and connects on startup, and its tools surface to the model
under the project's MCP namespace — i.e. it is presented as a project MCP
while its definition and credentials live in the global credentials store.
Where it persists
The only non-volatile file on disk holding this transport is the global~/.claude/.credentials.json, under an mcpOAuth key:
{
"mcpOAuth": {
"forge|<hash>": { "serverName": "forge", "serverUrl": "https://<REDACTED-HOST>/mcp/", "accessToken": "<REDACTED>", "discoveryState": { ... } },
"forge|<hash2>": { "serverName": "forge", "serverUrl": "http://localhost:8000/mcp", ... }
},
"claudeAiOauth": {
"accessToken": "<REDACTED sk-ant-oat01-...>",
"refreshToken": "<REDACTED sk-ant-ort01-...>"
}
}
Impact
- Undeclared egress of credentials. An HTTP transport the user did not
declare sends an API key (X-API-Key) to a remote host on every startup.
- Misrepresentation of scope. The transport is surfaced as a project
MCP while it is actually sourced from the global credentials file — the
user cannot reason about its origin from their project config.
- Non-removable via config. The entry survives full restarts even after
the user removed the MCP configuration; the only place it lives is the
global .credentials.json, which a normal user does not know to edit.
- Plaintext secrets at rest. OAuth access/refresh tokens, an MCP API key,
and a credential envelope are stored in plaintext in a global file.
Expected behavior
- Only load MCP servers from configuration the user declared, in the scope
(project/user) they declared.
- The server scope/origin shown to the model/UI must match where it is
actually defined; a globally-stored server must not be presented as a
project MCP.
- Removing an MCP server from configuration must stop it from loading; no
shadow copy in .credentials.json should resurrect it.
- Credential material must not be retained after the corresponding server is
removed, and should not be at rest in plaintext.
Repro
- Project
.mcp.jsondeclares only a stdio MCP server. - Run with
--debug: an additional HTTP MCP server initializes and
connects, with an X-API-Key header, not present in any .mcp.json /
user mcpServers.
- Inspect
~/.claude/.credentials.json→mcpOAuth→ the entry is here. - Remove the MCP config / restart repeatedly → the HTTP server still loads.
Notes
All tokens, keys, hostnames, and the credential envelope are redacted in this
report. The reporter is rotating the exposed credentials.
This issue has 1 comment on GitHub. Read the full discussion on GitHub ↗