Skip to content

fix(claude): honor CLAUDE_CONFIG_DIR for user MCP config#2096

Merged
danielmeppiel merged 4 commits into
mainfrom
fix/claude-config-dir-mcp
Jul 10, 2026
Merged

fix(claude): honor CLAUDE_CONFIG_DIR for user MCP config#2096
danielmeppiel merged 4 commits into
mainfrom
fix/claude-config-dir-mcp

Conversation

@danielmeppiel

@danielmeppiel danielmeppiel commented Jul 9, 2026

Copy link
Copy Markdown
Collaborator

fix(claude): honor CLAUDE_CONFIG_DIR for user MCP config

TL;DR

Claude user-scope MCP writes now follow CLAUDE_CONFIG_DIR when it is set and non-blank, falling back to ~/.claude.json otherwise. This keeps apm install -g --mcp aligned with the file Claude Code actually reads and closes #2060.

Note

Project-scope behavior remains unchanged: Claude MCP entries still go to .mcp.json.

Problem (WHY)

  • ClaudeClientAdapter(user_scope=True) hardcoded ~/.claude.json, even when Claude Code was configured to read $CLAUDE_CONFIG_DIR/.claude.json.
  • Global MCP installs could report success while writing a file Claude Code did not read.
  • [!] Other Claude user-scope primitives already honored CLAUDE_CONFIG_DIR, so MCP routing diverged from the target's established behavior.

Issue #2060 captures the observed contract: user-scope MCP servers should be written to $CLAUDE_CONFIG_DIR/.claude.json when configured, with the existing home-directory fallback preserved.

Approach (WHAT)

# Fix
1 Resolve the Claude user config root from a trimmed CLAUDE_CONFIG_DIR.
2 Preserve ~/.claude.json for unset or whitespace-only values.
3 Add a regression test that exercises the adapter's public config-path result.
4 Update user-facing MCP and environment-variable guidance.

Implementation (HOW)

File Change
src/apm_cli/adapters/client/claude.py _user_claude_json_path() now requires an absolute non-blank CLAUDE_CONFIG_DIR, normalizes it, and logs the resolved write path; all existing read, write, and merge paths continue through this helper.
tests/unit/test_claude_mcp.py Adds path-resolution and relative-path rejection coverage.
tests/integration/test_mcp_targets_gating_e2e.py Exercises a real user-scope MCP install and proves the relocated file is written without leaking to the home fallback.
docs/src/content/docs/consumer/install-mcp-servers.md Documents the Claude global MCP destination and fallback.
docs/src/content/docs/reference/environment-variables.md Extends the variable contract to include user-scope MCP config.
packages/apm-guide/.apm/skills/apm-usage/dependencies.md Mirrors the MCP destination guidance in the bundled usage skill.

Diagrams

Legend: The dashed node is the new environment-aware destination selected before the existing Claude MCP write path runs.

flowchart LR
    E[CLAUDE_CONFIG_DIR] --> Q{Non-blank value}
    Q -->|yes| C[Config directory .claude.json]
    Q -->|no| H[Home directory .claude.json]
    C --> W[Existing Claude MCP read and write path]
    H --> W
    classDef new stroke-dasharray: 5 5;
    class C new;
Loading

Trade-offs

  • Local resolution over cross-module reuse. The adapter reads the environment directly instead of coupling MCP config writes to the broader deployment-target resolver; this is the smallest sibling-consistent fix.
  • Dynamic lookup over constructor caching. The helper resolves the variable when the path is requested, preserving current adapter behavior and making environment-scoped tests deterministic.
  • No CHANGELOG entry. The fix is documented in the directly affected reference and bundled usage guidance; release maintainers can consolidate the final release note.

Benefits

  1. A non-blank CLAUDE_CONFIG_DIR produces exactly one expected user MCP destination: $CLAUDE_CONFIG_DIR/.claude.json.
  2. Unset and blank values retain the existing ~/.claude.json destination.
  3. Reads, merges, stale checks, and writes share the corrected helper without separate routing logic.
  4. The regression test fails when the environment lookup is removed or renamed.

Validation

uv run --extra dev pytest tests/unit/test_claude_mcp.py -q:

29 passed, 4 subtests passed in 0.84s

uv run --extra dev pytest tests/unit/test_claude_mcp.py tests/integration/test_claude_mcp_schema_fidelity.py tests/integration/test_mcp_targets_gating_e2e.py -q:

46 passed, 4 subtests passed in 3.34s
Canonical lint mirror
All checks passed!
1410 files already formatted
Your code has been rated at 10.00/10
[+] auth-signal lint clean
All CI grep guardrails passed

Mutation-break proof replaced CLAUDE_CONFIG_DIR with CLAUDE_CONFIG_DIR_MUTANT; test_user_scope_claude_install_honors_claude_config_dir failed because the relocated file was absent, then passed after restoration.

Scenario Evidence

# Scenario (user promise) Principle(s) Test(s) proving it Type
1 Set CLAUDE_CONFIG_DIR, then install Claude MCP servers globally; APM writes that directory's .claude.json and not the home fallback. Portability by manifest, Multi-harness support, DevX tests/integration/test_mcp_targets_gating_e2e.py::TestMCPTargetsGatingE2E::test_user_scope_claude_install_honors_claude_config_dir (regression-trap for #2060) integration-with-fixtures

How to test

  • Set CLAUDE_CONFIG_DIR to a scratch directory and instantiate ClaudeClientAdapter(user_scope=True); get_config_path() should end in that directory's .claude.json.
  • Unset CLAUDE_CONFIG_DIR; the same call should return ~/.claude.json.
  • Run the Claude MCP unit and integration suites; all tests and subtests should pass.
  • Run the canonical lint mirror; every gate should exit zero.

Closes #2060

Co-authored-by: Copilot 223556219+Copilot@users.noreply.github.com

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot AI review requested due to automatic review settings July 9, 2026 22:18

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR fixes Claude Code user-scope MCP config routing so global MCP installs honor CLAUDE_CONFIG_DIR (when set to a non-blank value) and otherwise fall back to ~/.claude.json, aligning APM behavior with the file Claude Code reads (issue #2060).

Changes:

  • Update ClaudeClientAdapter user-scope config path resolution to consult CLAUDE_CONFIG_DIR (trimmed) before falling back to Path.home() / ".claude.json".
  • Add a regression test ensuring get_config_path() respects CLAUDE_CONFIG_DIR and isolate the default-path tests from the ambient environment.
  • Update docs and bundled usage guidance to document the Claude MCP destination and fallback behavior.
Show a summary per file
File Description
src/apm_cli/adapters/client/claude.py Resolve the user-scope Claude MCP config path via CLAUDE_CONFIG_DIR when set.
tests/unit/test_claude_mcp.py Add regression coverage for CLAUDE_CONFIG_DIR path routing and stabilize tests against caller env.
docs/src/content/docs/consumer/install-mcp-servers.md Document Claude global MCP destination honoring CLAUDE_CONFIG_DIR with fallback.
docs/src/content/docs/reference/environment-variables.md Extend CLAUDE_CONFIG_DIR docs to include user-scope MCP config behavior.
packages/apm-guide/.apm/skills/apm-usage/dependencies.md Mirror the Claude MCP destination guidance in the bundled usage skill.

Review details

  • Files reviewed: 5/5 changed files
  • Comments generated: 1
  • Review effort level: Low

Comment thread src/apm_cli/adapters/client/claude.py Outdated
Comment on lines +145 to +147
config_dir = os.environ.get("CLAUDE_CONFIG_DIR", "").strip()
if config_dir:
return Path(config_dir).expanduser() / ".claude.json"
@danielmeppiel

Copy link
Copy Markdown
Collaborator Author

APM Review Panel: needs_rework

The path fix is focused and documented, but the install destination still lacks the required empirical integration proof.

cc @sergio-sisternes-epam -- a fresh advisory pass is ready for your review.

The panel agrees the adapter change is directionally correct. The load-bearing gap is test tier: the unit test proves path selection, but not that a user-scope MCP write creates $CLAUDE_CONFIG_DIR/.claude.json while leaving $HOME/.claude.json untouched. The operator gate requires that integration-with-fixtures evidence and a mutation-break proof before shipping.

The path-hardening and diagnostic recommendations are small and remain inside this PR's scope.

Dissent. The documentation lens rated the missing integration proof blocking-severity while architecture and coverage rated it recommended. The explicit operator gate resolves that disagreement: the proof must be added.

Aligned with: portable-by-manifest through runtime-native config placement; multi-harness support through Claude-specific user-scope routing; pragmatic-as-npm through predictable environment overrides.

Panel summary

Persona B R N Takeaway
Python Architect 0 1 1 Clean path override; add integration proof and isolate ambient env.
CLI Logging Expert 0 1 1 Surface the resolved destination in diagnostics.
DevX UX Expert 0 1 1 Reject ambiguous relative config roots.
Supply Chain Security Expert 0 1 1 Normalize the configured root before writing.
OSS Growth Hacker 0 0 2 Accurate docs; only minor discoverability polish remains.
Doc Writer 1 0 0 The documented install-path promise needs empirical proof.
Test Coverage Expert 0 1 1 Unit proof is below the install-pipeline tier floor.

B = blocking-severity findings, R = recommended, N = nits.
Counts are signal strength, not gates. The maintainer ships.

Top 4 follow-ups

  1. [Test Coverage Expert] (blocking-severity) Add an integration-with-fixtures test that performs the user-scope write, asserts $CLAUDE_CONFIG_DIR/.claude.json, and asserts $HOME/.claude.json is absent; prove it with mutation-break.
  2. [DevX UX Expert] Reject relative CLAUDE_CONFIG_DIR values with a clear error, matching the sibling Codex adapter.
  3. [Supply Chain Security Expert] Normalize the configured root with resolve(strict=False) before use.
  4. [CLI Logging Expert] Add a diagnostic breadcrumb containing the resolved user-scope config path.

Architecture

classDiagram
    MCPClientAdapter <|-- CopilotClientAdapter
    CopilotClientAdapter <|-- ClaudeClientAdapter
    ClaudeClientAdapter : +_user_claude_json_path() Path
    ClaudeClientAdapter : +_merge_user_mcp(config_updates) bool
    ClaudeClientAdapter ..> atomic_write_text : user-scope writes
Loading
flowchart TD
    A[apm install -g --mcp] --> B[ClaudeClientAdapter user scope]
    B --> C{CLAUDE_CONFIG_DIR nonblank}
    C -->|yes| D[configured root .claude.json]
    C -->|no| E[HOME .claude.json]
    D --> F[atomic write 0600]
    E --> F
Loading

Recommendation

Add the empirical integration test and mutation-break proof, then fold the bounded path-hardening and diagnostic items. Re-run the panel on the resulting SHA.


Full per-persona findings

Python Architect

  • [recommended] Missing integration-with-fixtures proof for relocated user-scope MCP writes at tests/integration/test_claude_mcp_schema_fidelity.py.
  • [nit] The user-scope integration fixture does not clear ambient CLAUDE_CONFIG_DIR.

CLI Logging Expert

  • [recommended] The new destination choice lacks a resolved-path diagnostic breadcrumb.
  • [nit] Direct _rich_success usage is pre-existing and outside the essential fix.

DevX UX Expert

  • [recommended] Relative CLAUDE_CONFIG_DIR values resolve against CWD and should be rejected.
  • [nit] The Claude table cell is long but remains accurate.

Supply Chain Security Expert

  • [recommended] Normalize the configured root consistently with the target resolver.
  • [nit] The unit test does not exercise a real write.

OSS Growth Hacker

  • [nit] Consider a future footnote pattern if runtime path cells grow further.
  • [nit] A reference cross-link could improve discovery, but is not needed for correctness.

Auth Expert -- inactive

No authentication surface is affected by this path-routing fix.

Doc Writer

  • [blocking] The documented global Claude MCP destination has no integration-level proof.

Test Coverage Expert

  • [recommended] The install-pipeline promise needs integration-with-fixtures evidence.
  • [nit] The new unit test passes and correctly proves helper-level path selection.

Performance Expert -- inactive

No package-manager performance surface is affected.

This panel is advisory. It does not block merge. Re-apply the panel-review label after addressing feedback to re-run.

danielmeppiel and others added 3 commits July 10, 2026 02:49
Exercise the real user-scope MCP install path, harden the configured root, and surface its resolved destination in diagnostics. Addresses the review panel follow-ups for PR #2096.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Clear ambient CLAUDE_CONFIG_DIR in schema-fidelity fixtures so default-path integration tests remain hermetic. Addresses the Python architect panel finding.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Distinguish blank fallback from relative-path rejection and make the validation error actionable. Addresses the final panel documentation and recovery findings.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@danielmeppiel

Copy link
Copy Markdown
Collaborator Author

APM Review Panel: ship_now

All active panelists returned zero findings after the empirical install proof and bounded follow-ups were folded.

cc @sergio-sisternes-epam -- a fresh advisory pass is ready for your review.

The panel is unanimous on exact SHA b2926baa41b2d4dd23ece869eb7ec63369c82d27. The real MCPIntegrator.install regression test proves that a Claude user-scope MCP install writes $CLAUDE_CONFIG_DIR/.claude.json, preserves the expected server content, and does not create $HOME/.claude.json. Path validation, normalization, diagnostics, fixture isolation, and documentation are aligned. Full lint and CI are green.

Aligned with: portable-by-manifest through runtime-native user config placement; multi-harness support through Claude-specific routing; secure-by-default through absolute normalized paths and atomic 0600 writes; pragmatic-as-npm through an actionable environment override.

Panel summary

Persona B R N Takeaway
Python Architect 0 0 0 Minimal adapter extension; no architectural concerns remain.
CLI Logging Expert 0 0 0 Diagnostic breadcrumb and recovery message are clear.
DevX UX Expert 0 0 0 Override, fallback, and rejection behavior are predictable.
Supply Chain Security Expert 0 0 0 Normalized path and atomic restricted write preserve safety.
OSS Growth Hacker 0 0 0 Documentation stays accurate and scannable.
Doc Writer 0 0 0 Code, reference docs, and bundled guidance agree.
Test Coverage Expert 0 0 0 Integration and unit evidence defend every changed behavior.

B = blocking-severity findings, R = recommended, N = nits.
Counts are signal strength, not gates. The maintainer ships.

Architecture

classDiagram
    MCPClientAdapter <|-- CopilotClientAdapter
    CopilotClientAdapter <|-- ClaudeClientAdapter
    ClaudeClientAdapter : +_user_claude_json_path() Path
    ClaudeClientAdapter : +_merge_user_mcp(config_updates) bool
    ClaudeClientAdapter ..> PathTraversalError : rejects relative roots
    ClaudeClientAdapter ..> atomic_write_text : writes user config
Loading
flowchart TD
    A[apm install -g --mcp] --> B[Claude user-scope adapter]
    B --> C{CLAUDE_CONFIG_DIR nonblank}
    C -->|yes| D[expand validate and normalize]
    C -->|no| E[HOME .claude.json]
    D --> F[configured root .claude.json]
    E --> G[atomic write 0600]
    F --> G
Loading

Recommendation

The scoped behavior is empirically proven, mutation-protected, documented, and green on the final SHA. Ready for maintainer review.

Folded in this run

  • (panel) Added real user-scope MCP install coverage for relocated output and absent home fallback -- resolved in 4a5e94b2c777d6c6b40df3e6942143fe3d392b2b.
  • (panel) Rejected relative CLAUDE_CONFIG_DIR values and normalized accepted paths -- resolved in 4a5e94b2c777d6c6b40df3e6942143fe3d392b2b.
  • (panel) Added a debug breadcrumb for the resolved Claude user config destination -- resolved in 4a5e94b2c777d6c6b40df3e6942143fe3d392b2b.
  • (panel) Isolated schema-fidelity tests from ambient CLAUDE_CONFIG_DIR -- resolved in 59bd501820db98cd04e99996c64233a87b35b31f.
  • (panel) Made relative-path recovery guidance actionable -- resolved in b2926baa41b2d4dd23ece869eb7ec63369c82d27.
  • (panel) Clarified blank fallback versus relative-path rejection across docs and bundled guidance -- resolved in b2926baa41b2d4dd23ece869eb7ec63369c82d27.
  • (panel) Shortened the Claude destination matrix entry while preserving the full contract in prose -- resolved in b2926baa41b2d4dd23ece869eb7ec63369c82d27.

Regression-trap evidence (mutation-break gate)

  • tests/integration/test_mcp_targets_gating_e2e.py::TestMCPTargetsGatingE2E::test_user_scope_claude_install_honors_claude_config_dir -- replaced the CLAUDE_CONFIG_DIR lookup with CLAUDE_CONFIG_DIR_MUTANT; the test FAILED because the relocated file was absent; the guard was restored and the test passed.

Lint contract

The canonical ruff pair, YAML I/O guard, file-length guard, portable-relative-path guard, pylint R0801 guard, and auth-signals guard all passed before the final push.

CI

All checks passed on exact SHA b2926baa41b2d4dd23ece869eb7ec63369c82d27: https://github.com/microsoft/apm/actions/runs/29061703296 (0 CI recovery iterations).

Mergeability status

PR head SHA CEO stance iters folds defers Copilot rounds CI mergeable mergeStateStatus notes
#2096 b2926ba ship_now 2 7 0 2 green MERGEABLE BLOCKED awaiting required review

Convergence

2 outer iterations; 2 Copilot rounds with zero inline findings. Final panel verdict: ship_now.


Full per-persona findings

Python Architect

No findings.

CLI Logging Expert

No findings.

DevX UX Expert

No findings.

Supply Chain Security Expert

No findings.

OSS Growth Hacker

No findings.

Auth Expert -- inactive

No authentication or credential surface is touched.

Doc Writer

No findings.

Test Coverage Expert

No findings.

Performance Expert -- inactive

No cache, transport, materialization, parallelism, or performance surface is touched.

This panel is advisory. It does not block merge. Re-apply the panel-review label after addressing feedback to re-run.

@danielmeppiel danielmeppiel merged commit b29c625 into main Jul 10, 2026
15 checks passed
@danielmeppiel danielmeppiel deleted the fix/claude-config-dir-mcp branch July 10, 2026 04:27
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[BUG] claude MCP writer ignores CLAUDE_CONFIG_DIR, writes to ~/.claude.json instead of $CLAUDE_CONFIG_DIR/.claude.json

2 participants