From 17e4321761bd1e17ff01bf6de0c8be8e61be5962 Mon Sep 17 00:00:00 2001 From: nntrivi2001 Date: Wed, 9 Sep 2026 09:54:27 +0700 Subject: [PATCH] fix(cc-config): read .claude.json from the same root the CLI writes it to getClaudeJsonPath() was the only one of cc-discovery's 20 read paths not derived from getClaudeHome(), so a CLAUDE_HOME install read one file while the CLI wrote another. The default case was fixed by dropping the CLAUDE_CONFIG_DIR pin; this closes the mirrored half. Also replaces the Remove button's display-string comparison with a removable flag from the reader: rewording the source label silently resurrected a button that always fails. --- client/src/lib/api.ts | 5 +++++ client/src/pages/CcConfig.tsx | 9 +++++---- server/__tests__/cc-plugin-cli.test.js | 23 +++++++++++++++++++++++ server/lib/cc-discovery.js | 25 +++++++++++++++---------- 4 files changed, 48 insertions(+), 14 deletions(-) diff --git a/client/src/lib/api.ts b/client/src/lib/api.ts index a646eccc..ad5456b2 100644 --- a/client/src/lib/api.ts +++ b/client/src/lib/api.ts @@ -2651,6 +2651,11 @@ export interface CcMcpServer { name: string; /** Which config file this entry came from (e.g. a `.mcp.json` path). */ source: string; + /** + * False when no `claude mcp` scope can address this entry, so the dashboard + * cannot remove it. Absent means removable — only the reader sets it. + */ + removable?: boolean; /** Transport: local subprocess ("stdio"), remote HTTP, or undetermined. */ kind: "stdio" | "http" | "unknown"; /** Launch command, for `kind === "stdio"`. */ diff --git a/client/src/pages/CcConfig.tsx b/client/src/pages/CcConfig.tsx index f81f3067..35c53411 100644 --- a/client/src/pages/CcConfig.tsx +++ b/client/src/pages/CcConfig.tsx @@ -2022,10 +2022,11 @@ function McpCard({ }) { const [confirmRemove, setConfirmRemove] = useState(false); const busyId = `${scope}:${server.name}`; - // Entries from ~/.claude/settings.json have no CLI scope that addresses - // them (`claude mcp remove` only ever touches ~/.claude.json / .mcp.json) - // — a Remove button here would look live and never work. - const removable = server.source !== "~/.claude/settings.json"; + // The reader decides this: entries from ~/.claude/settings.json have no CLI + // scope that addresses them, so a Remove button would look live and never + // work. Reading it off a flag rather than matching `source` keeps a reworded + // display string from silently bringing that button back. + const removable = server.removable !== false; return (
diff --git a/server/__tests__/cc-plugin-cli.test.js b/server/__tests__/cc-plugin-cli.test.js index d72d83a1..dd6946d8 100644 --- a/server/__tests__/cc-plugin-cli.test.js +++ b/server/__tests__/cc-plugin-cli.test.js @@ -224,6 +224,29 @@ describe("cc-plugin-cli CLAUDE_CONFIG_DIR pinning", () => { else process.env.CLAUDE_HOME = originalHome; } }); + + // The two env tests above pin one half each. This pins the pair: the file + // the dashboard READS must be the file the CLI WRITES, under both settings. + // `.claude.json` was the only read path in cc-discovery not derived from + // getClaudeHome(), which is exactly how the two halves drifted apart. + it("reads the same .claude.json the CLI writes, with CLAUDE_HOME set or unset", () => { + const cc = require("../lib/cc-discovery"); + const originalHome = process.env.CLAUDE_HOME; + + // CLI's write target = /.claude.json, and + // CLAUDE_CONFIG_DIR is either CLAUDE_HOME or the CLI's own default, + // which is the same directory getClaudeHome() returns. + try { + delete process.env.CLAUDE_HOME; + assert.equal(cc.getClaudeJsonPath(), path.join(os.homedir(), ".claude", ".claude.json")); + + process.env.CLAUDE_HOME = "/tmp/some-other-claude-home"; + assert.equal(cc.getClaudeJsonPath(), "/tmp/some-other-claude-home/.claude.json"); + } finally { + if (originalHome === undefined) delete process.env.CLAUDE_HOME; + else process.env.CLAUDE_HOME = originalHome; + } + }); }); describe("cc-plugin-cli run outcomes", () => { diff --git a/server/lib/cc-discovery.js b/server/lib/cc-discovery.js index 053f489c..02b1cd41 100644 --- a/server/lib/cc-discovery.js +++ b/server/lib/cc-discovery.js @@ -29,16 +29,13 @@ function getProjectClaudeDir(cwd) { } function getClaudeJsonPath() { - // ~/.claude.json sits beside ~/.claude/, NOT inside it. Resolve from $HOME - // so a CLAUDE_HOME override doesn't accidentally relocate it. - // - // Paired with cc-plugin-cli.js's runClaude(): with CLAUDE_HOME unset (the - // default), it deliberately leaves CLAUDE_CONFIG_DIR unset too, so the CLI - // writes here (its own default). With CLAUDE_HOME set, this read stays - // pinned to $HOME/.claude.json while the CLI writes - // /.claude.json — a known, unfixed split for that case; see - // runClaude's comment. - return path.join(os.homedir(), ".claude.json"); + // One root for everything: this file used to resolve from $HOME while the + // other 19 read paths derived from getClaudeHome(), and cc-plugin-cli.js's + // runClaude() pinned CLAUDE_CONFIG_DIR to getClaudeHome() — which is where + // the CLI puts .claude.json. The dashboard read one file and made the CLI + // write another, so MCP add/remove was inert. Keep this and runClaude's + // env in step; they are the same decision seen from two sides. + return path.join(getClaudeHome(), ".claude.json"); } /** @@ -516,6 +513,11 @@ function readMcpServers(opts = {}) { out.user.push({ name, source: "~/.claude/settings.json", + // No `claude mcp` scope addresses settings.json, so the dashboard + // cannot remove these. A flag, not a string comparison on `source`: + // the UI used to match that display text, so rewording it silently + // brought back a Remove button that always fails. + removable: false, ...summarizeMcpDef(def), }); } @@ -911,6 +913,9 @@ function readOverview(opts = {}) { } module.exports = { + // Exported so a test can pin it against the file the CLI writes — the two + // drifted apart once and the mismatch was invisible from either side alone. + getClaudeJsonPath, // surface readers readSkills, readAgents,