[BUG] claude mcp remove reformats .mcp.json to reveal secrets

Status Fixed / completed
Reported on v2.1.133
Maintainer reply None cached
Activity 2 comments · opened May 8, 2026 · closed May 9, 2026

Preflight Checklist

  • [x] I have searched existing issues and this hasn't been reported yet
  • [x] This is a single bug report (please file separate reports for different bugs)
  • [x] I am using the latest version of Claude Code

What's Wrong?

Closely related to #18692.

This bug is still reproducing in Claude Code 2.1.133, and the broken code path is broader than just claude mcp add — claude mcp remove exhibits identical behavior. Suggest reopening this issue rather than tracking it separately, since the underlying defect is the same.

New finding: claude mcp remove also expands ${VAR} placeholders

Running claude mcp remove <name> against a project-scoped .mcp.json rewrites the file with all unrelated ${VAR} placeholders expanded into their literal env values.

Implication for the fix

Two unrelated config-mutating subcommands (add from the older issue, remove from my repro) exhibit identical expand-and-persist behavior. That puts the defect in the shared read-mutate-write path for .mcp.json, not in either subcommand's arg-parsing surface. The shell-quoting workaround mentioned earlier in this thread ('API_KEY=$MY_SECRET') doesn't apply to mcp remove — no env-var args are passed in.

Suggested fixes (either is sufficient, both close the class):

  1. Preserve raw on-disk text for unmodified entries — only modify the targeted block during write-back.
  2. Defer all ${VAR} resolution to MCP-launch time; never persist resolved values back to the config file.

Worth auditing the entire claude mcp * subcommand surface for the same pattern (set, get, scope changes, etc.).

What Should Happen?

No leakage.

Error Messages/Logs

Steps to Reproduce

Repro: a project .mcp.json containing three entries, two with ${VAR} placeholders in env and one with none (the removal target). After claude mcp remove linear, the linear block is removed correctly, but the OTHER entries' secrets get baked in:

       "env": {
-        "LANGSMITH_API_KEY": "${LANGSMITH_API_KEY}",
-        "LANGSMITH_WORKSPACE_ID": "${LANGSMITH_WORKSPACE_ID}",
+        "LANGSMITH_API_KEY": "lsv2_pt_<REDACTED-32-hex>_<REDACTED-10-hex>",
+        "LANGSMITH_WORKSPACE_ID": "<REDACTED-uuid>",
         "LANGSMITH_ENDPOINT": "https://api.smith.langchain.com"
       }
     },
     "mcp-atlassian": {
       ...
       "env": {
-        "JIRA_URL": "${JIRA_URL}",
-        "JIRA_USERNAME": "${JIRA_USERNAME}",
-        "JIRA_API_TOKEN": "${JIRA_API_TOKEN}"
+        "JIRA_URL": "https://<REDACTED-org>.atlassian.net",
+        "JIRA_USERNAME": "<REDACTED-email>",
+        "JIRA_API_TOKEN": "ATATT3xFfGF0<REDACTED-full-jira-pat>=<REDACTED-suffix>"
       }
     },
-    "linear": { ... },

The <REDACTED-...> placeholders correspond to real production secrets in my repro — a LangSmith API key and a full Jira API token. I caught them in git diff before committing. Anyone running git commit -am ... after a routine mcp remove would have leaked them.

Self-contained reproducer

Bundle with before.mcp.json, after.mcp.json, full unified diff, synthetic env values, and a steps.sh reproducer script:

claude-mcp-remove-bug-report.zip

The bundle uses synthetic env values that preserve the character pattern of real secrets but have no value to a third party — running steps.sh produces an identical-shape diff to the redacted one above, with no real credentials involved.

Claude Model

Opus

Is this a regression?

I don't know

Last Working Version

_No response_

Claude Code Version

2.1.133

Platform

Anthropic API

Operating System

macOS

Terminal/Shell

Cursor

Additional Information

_No response_

View original on GitHub ↗

This issue has 2 comments on GitHub. Read the full discussion on GitHub ↗