Skip to content

fix(claude): honor CLAUDE_CONFIG_DIR for user-scope MCP config and the plugin cache#376

Merged
mbektas merged 2 commits into
plmbr:mainfrom
pjdoland:fix/375-ccd-mcp-plugins
Jun 16, 2026
Merged

fix(claude): honor CLAUDE_CONFIG_DIR for user-scope MCP config and the plugin cache#376
mbektas merged 2 commits into
plmbr:mainfrom
pjdoland:fix/375-ccd-mcp-plugins

Conversation

@pjdoland

Copy link
Copy Markdown
Collaborator

Summary

Follow-up to #374, finishing the CLAUDE_CONFIG_DIR work from #373. Two surfaces still hardcoded home-relative paths: the MCP management tab read user-scope servers (and wrote disabledMcpServers) at ~/.claude.json even though the CLI relocates that file to $CLAUDE_CONFIG_DIR/.claude.json when the override is set, so file reads and CLI-mediated writes (claude mcp add, which inherits the env) diverged on override deployments. The Plugins panel's cache fallback pointed at ~/.claude/plugins instead of the relocated cache.

Stacked on #374; this branch contains that PR's commit until it merges, after which I'll rebase so only this change remains. Merge #374 first.

Solution

The load-bearing changes are two resolvers. claude_mcp_manager.py gains a private _claude_user_config_path() rather than reusing util.get_claude_config_dir, because the file's default genuinely differs: it lives in the home dir itself ($HOME/.claude.json), not inside ~/.claude, while the override moves it inside the config dir. plugin_manager.py's _claude_plugins_root() fallback now routes through the shared helper; CLAUDE_CODE_PLUGIN_CACHE_DIR keeps precedence over the config dir, which matches the CLI's own ordering.

Both behaviors were verified against the installed CLI, not assumed: the CLI bundle's user-config getter is join(process.env.CLAUDE_CONFIG_DIR || homedir(), ".claude.json") and its plugins root checks the cache-dir var before configDir/plugins; a sandboxed claude mcp add -s user under an override wrote to $dir/.claude.json and never touched the fake $HOME.

The change also surfaced a real test hazard, fixed here: the unittest-style PATCH endpoint suite patches Path.home but pytest fixtures don't reach AsyncHTTPTestCase, so a developer with CLAUDE_CONFIG_DIR exported would have had the test suite write junk into their real relocated .claude.json. setUp now snapshots and scrubs the env, and the Playwright webServer env drops the variable for the same seam (deleting it rather than blanking it, because the CLI treats an empty string as a set, degenerate config dir).

Per your request this PR also dates 5.1.0 in the changelog (released 2026-06-08) and adds the missing [5.1.0] compare link, retargeting [unreleased].

Testing

  • Six new tests: TestClaudeUserConfigPath (reads the relocated config; override wins over a populated home config; default unchanged) and TestClaudePluginsRoot (falls back to $CLAUDE_CONFIG_DIR/plugins; explicit cache dir wins; home default unchanged with HOME/USERPROFILE pinned for POSIX and Windows).
  • Hostile-env runs: the two touched suites (171 tests) and the full suite pass with CLAUDE_CONFIG_DIR exported to a junk path, and the junk dir is never created by the touched tests (previously the PATCH test wrote real files there).
  • Full verification green: pytest 1183 (+57 claude_client separately), tsc --noEmit (root and ui-tests), lint:check, jest 352.
  • Live verification: started JupyterLab with CLAUDE_CONFIG_DIR pointing at a synthetic .claude.json; GET /notebook-intelligence/claude-mcp returned the user-scope server from the relocated file.

Risks / follow-ups

  • No behavior change without the env var: both fallbacks resolve to the same paths as before.
  • The MCP tab's info banner in src/components/claude-mcp-panel.tsx still says ~/.claude.json unconditionally; cosmetic copy, left for a small follow-up to keep this PR backend-scoped.
  • The admin guide's ~/.claude.json and ~/.claude/plugins/ mentions now carry the override notes, added in this PR once the code actually followed the override.

Fixes #375

@pjdoland pjdoland added the bug Something isn't working label Jun 12, 2026
@mbektas

mbektas commented Jun 16, 2026

Copy link
Copy Markdown
Collaborator

@pjdoland can you resolve conflicts

pjdoland added 2 commits June 16, 2026 06:07
The Claude CLI relocates its entire config dir, including session
transcripts under projects/, when CLAUDE_CONFIG_DIR is set. The session
pickers always read ~/.claude/projects, so on deployments that set the
override (JupyterHub images commonly point it at a persistent volume)
both pickers came up empty even though 'claude --resume' saw every
session.

Resolve the CLI's config dir through one shared util helper and make it
the claude_home default for get_sessions_dir and list_all_sessions. The
skills directory and spinner-verbs lookups already carried their own
copies of the same expression; they now use the helper too, so the
override semantics can't drift between surfaces. The helper deliberately
does not expanduser the env value (the CLI doesn't) and treats an empty
string as unset (matching the CLI's Node-style falsy check).

The MCP user config (~/.claude.json) and the plugin cache have the same
class of gap but need different relocation logic; tracked separately.

Fixes plmbr#373
…e plugin cache

The CLI relocates .claude.json to $CLAUDE_CONFIG_DIR/.claude.json when
the override is set, but the MCP manager kept reading (and writing
disabledMcpServers to) $HOME/.claude.json while CLI-mediated writes via
'claude mcp add' inherited the env and landed in the relocated file, so
reads and writes diverged on override deployments. The plugin cache
fallback similarly pointed at ~/.claude/plugins instead of the relocated
cache.

The .claude.json resolver is a private helper rather than a reuse of
util.get_claude_config_dir because its default genuinely differs: the
file lives in the home dir itself, not inside ~/.claude. The plugins
fallback does route through the shared helper; the CLI gives
CLAUDE_CODE_PLUGIN_CACHE_DIR precedence over the config dir and NBI
matches that, verified against the CLI bundle and a sandboxed
'claude mcp add' run.

Test hardening that the change made necessary: the unittest-style PATCH
suite patches Path.home but pytest fixtures don't reach it, so a
developer's exported CLAUDE_CONFIG_DIR would have sent the endpoint's
write into their real relocated .claude.json; setUp now snapshots and
scrubs the env. The Playwright webServer env drops the variable for the
same reason, deleting it rather than blanking it because the CLI treats
an empty string as a set (degenerate) config dir.

Also dates 5.1.0 in the changelog (released 2026-06-08) and adds the
missing compare links.

Fixes plmbr#375
@pjdoland pjdoland force-pushed the fix/375-ccd-mcp-plugins branch from e4b5da8 to 07fadc4 Compare June 16, 2026 10:10
@pjdoland

Copy link
Copy Markdown
Collaborator Author

Resolved conflicts.

@mbektas mbektas merged commit 9909c99 into plmbr:main Jun 16, 2026
4 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Remaining CLAUDE_CONFIG_DIR gaps: user-scope MCP config and plugin cache

2 participants