From 0ad4b8c920feaf09715d0de6381829f731f8114f Mon Sep 17 00:00:00 2001 From: nntrivi2001 Date: Wed, 9 Sep 2026 09:48:28 +0700 Subject: [PATCH] fix(cc-config): stop pinning CLAUDE_CONFIG_DIR when CLAUDE_HOME is unset MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit runClaude() unconditionally set CLAUDE_CONFIG_DIR to getClaudeHome() (~/.claude), so the CLI wrote ~/.claude/.claude.json — a file cc-discovery.getClaudeJsonPath() never reads (it reads ~/.claude.json, beside ~/.claude/, by design). Every default-install MCP add/remove and plugin/marketplace mutation was inert: add toasted success and vanished on refetch, remove 422'd for every server the panel lists. With CLAUDE_HOME unset (the default), leave CLAUDE_CONFIG_DIR unset too — the CLI's own default config dir already lines up with what cc-discovery reads, so alignment comes from doing nothing. Only pin it when the operator explicitly set CLAUDE_HOME, which is the one case the pin is meant for. Left a residual, documented at both sites: with CLAUDE_HOME set, reads still come from $HOME/.claude.json while the CLI writes /.claude.json — a split predating this task, affecting a configuration nobody here runs. New tests pin the env contract directly (no real CLI needed): runClaude must not set CLAUDE_CONFIG_DIR when CLAUDE_HOME is absent, and must set it to CLAUDE_HOME's value when present. --- server/__tests__/cc-plugin-cli.test.js | 36 ++++++++++++++++++++++++++ server/lib/cc-discovery.js | 7 +++++ server/lib/cc-plugin-cli.js | 18 ++++++++++--- 3 files changed, 58 insertions(+), 3 deletions(-) diff --git a/server/__tests__/cc-plugin-cli.test.js b/server/__tests__/cc-plugin-cli.test.js index 90ba3578..d72d83a1 100644 --- a/server/__tests__/cc-plugin-cli.test.js +++ b/server/__tests__/cc-plugin-cli.test.js @@ -190,6 +190,42 @@ describe("cc-plugin-cli argv construction", () => { }); }); +describe("cc-plugin-cli CLAUDE_CONFIG_DIR pinning", () => { + // N1: pinning CLAUDE_CONFIG_DIR unconditionally to getClaudeHome() made the + // CLI write ~/.claude/.claude.json while cc-discovery reads ~/.claude.json + // — a different file, so every default-install add/remove was inert. The + // fix is to leave CLAUDE_CONFIG_DIR unset unless the operator explicitly + // set CLAUDE_HOME; this is the env contract that would have caught it. + const PRINT_ENV = "printf '%s\\n' \"CLAUDE_CONFIG_DIR=${CLAUDE_CONFIG_DIR-}\""; + + it("leaves CLAUDE_CONFIG_DIR unset when CLAUDE_HOME is not set", async () => { + const originalHome = process.env.CLAUDE_HOME; + delete process.env.CLAUDE_HOME; + const restore = stubClaude(PRINT_ENV); + try { + const result = await cli.marketplaceUpdate({ name: "m" }); + assert.equal(result.stdout.trim(), "CLAUDE_CONFIG_DIR="); + } finally { + restore(); + if (originalHome !== undefined) process.env.CLAUDE_HOME = originalHome; + } + }); + + it("pins CLAUDE_CONFIG_DIR to CLAUDE_HOME when the operator set it", async () => { + const originalHome = process.env.CLAUDE_HOME; + process.env.CLAUDE_HOME = "/tmp/some-other-claude-home"; + const restore = stubClaude(PRINT_ENV); + try { + const result = await cli.marketplaceUpdate({ name: "m" }); + assert.equal(result.stdout.trim(), "CLAUDE_CONFIG_DIR=/tmp/some-other-claude-home"); + } finally { + restore(); + if (originalHome === undefined) delete process.env.CLAUDE_HOME; + else process.env.CLAUDE_HOME = originalHome; + } + }); +}); + describe("cc-plugin-cli run outcomes", () => { it("reports a non-zero exit as ok:false instead of throwing", async () => { const restore = stubClaude('echo "✘ Failed to install plugin \\"nope\\"" >&2\nexit 1'); diff --git a/server/lib/cc-discovery.js b/server/lib/cc-discovery.js index bfdf7514..053f489c 100644 --- a/server/lib/cc-discovery.js +++ b/server/lib/cc-discovery.js @@ -31,6 +31,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"); } diff --git a/server/lib/cc-plugin-cli.js b/server/lib/cc-plugin-cli.js index e462fe77..ac7aebee 100644 --- a/server/lib/cc-plugin-cli.js +++ b/server/lib/cc-plugin-cli.js @@ -131,15 +131,27 @@ function serialize(fn) { function runClaude(args, opts = {}) { const timeout = opts.timeoutMs || DEFAULT_TIMEOUT_MS; return new Promise((resolve, reject) => { + // Only pin CLAUDE_CONFIG_DIR when the operator explicitly set CLAUDE_HOME + // (a non-default install). Left unset, the CLI's own default config dir + // is `~/.claude` and it writes `.claude.json` NEXT TO it (`~/.claude.json`) + // — the exact file cc-discovery.getClaudeJsonPath() reads (see that + // function's own comment). Pinning CLAUDE_CONFIG_DIR unconditionally to + // getClaudeHome() made the CLI write `~/.claude/.claude.json` instead — + // a file nothing reads — so every default-install add/remove was inert. + // When CLAUDE_HOME IS set, this still pins it, but note the residual: + // cc-discovery's read stays pinned to $HOME/.claude.json regardless (see + // getClaudeJsonPath's comment) while the CLI then writes + // /.claude.json — a pre-existing split this task does not + // attempt to fix, since no install in this repo runs with CLAUDE_HOME set. + const env = { ...process.env }; + if (process.env.CLAUDE_HOME) env.CLAUDE_CONFIG_DIR = getClaudeHome(); const child = execFile( "claude", args, { timeout, maxBuffer: MAX_OUTPUT_BYTES, - // Pin the CLI to the same config dir cc-discovery reads, so a write - // made here is a write the dashboard can see. - env: { ...process.env, CLAUDE_CONFIG_DIR: getClaudeHome() }, + env, }, (err, stdout, stderr) => { const out = String(stdout || "");