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

Resolved 💬 2 comments Opened May 8, 2026 by alexgoodell 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 addclaude 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 ↗