Security: undeclared HTTP MCP transport persisted with credentials in global .credentials.json, presented as a project MCP, survives config removal

Open 💬 1 comment Opened Jun 20, 2026 by junhyeokchoi-enjcons

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.json declares 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 mcpServers block of ~/.claude.json (empty / no forge entry),
  • any per-project mcpServers block 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

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

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

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

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

  1. Project .mcp.json declares only a stdio MCP server.
  2. 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.

  1. Inspect ~/.claude/.credentials.jsonmcpOAuth → the entry is here.
  2. 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.

View original on GitHub ↗

This issue has 1 comment on GitHub. Read the full discussion on GitHub ↗