Describe the bug
At user scope (apm install -g), APM writes MCP servers for the Claude Code target to a hardcoded ~/.claude.json and does not consult CLAUDE_CONFIG_DIR. When CLAUDE_CONFIG_DIR is set (common in devcontainers and relocated setups), Claude Code reads user-scope MCP servers from $CLAUDE_CONFIG_DIR/.claude.json, so APM's writes land in a file Claude Code never reads. The servers silently fail to appear in /mcp, and because APM re-reads the same hardcoded file on the next run it reports them as "already configured (skipped)", which hides the problem.
This is inconsistent with the rest of APM: the user-scope target resolver in integration/targets.py already honors CLAUDE_CONFIG_DIR for every other primitive (skills, commands, agents, instructions, hooks). Only the MCP writer diverges.
Note on default behavior: with CLAUDE_CONFIG_DIR unset, Claude Code reads user config from $HOME/.claude.json (home root), so APM's hardcoded path happens to match. The bug only surfaces when CLAUDE_CONFIG_DIR is set, which relocates the file to $CLAUDE_CONFIG_DIR/.claude.json. That is exactly the scenario the variable exists for (devcontainers, relocated or shared config), so the mismatch is easy to hit and hard to notice.
To Reproduce
Steps to reproduce the behavior:
- Point Claude Code at a relocated config directory and confirm it takes effect:
export CLAUDE_CONFIG_DIR="$HOME/.claude" (any dir Claude Code uses). In a Claude Code session, /mcp shows the user MCP source as $CLAUDE_CONFIG_DIR/.claude.json.
- Install one or more MCP servers at global scope for the claude target:
apm install -g --mcp <some-server>.
- APM reports the server as configured (or "already configured" on a re-run).
- Start a new Claude Code session and run
/mcp. The server is absent.
- Confirm where APM actually wrote it:
jq -r '.mcpServers // {} | keys[]' "$HOME/.claude.json". The server is present in $HOME/.claude.json, not in $CLAUDE_CONFIG_DIR/.claude.json.
Expected behavior
MCP servers for the claude target at user scope are written to $CLAUDE_CONFIG_DIR/.claude.json when CLAUDE_CONFIG_DIR is set, falling back to ~/.claude.json when it is not. This matches the file Claude Code reads and mirrors the resolution APM already performs for other user-scope primitives. Instead, the servers are written to a file Claude Code never reads, and the "already configured" skip on re-runs makes the mismatch silent.
Environment (please complete the following information):
- OS: [e.g. Linux, devcontainer based on the node image]
- Python Version: [e.g. 3.12.0]
- APM Version: [your
apm --version; source references below are against 0.24.0, commit b915f8d]
- VSCode Version (if relevant): [n/a, reproduced with the Claude Code CLI]
- Claude Code Version: [your
claude --version]
CLAUDE_CONFIG_DIR: $HOME/.claude (set in a devcontainer, so Claude Code reads $HOME/.claude/.claude.json)
Logs
APM reports the servers as configured while /mcp does not list them. The two files diverge:
$ echo "$CLAUDE_CONFIG_DIR"
/home/node/.claude
# Claude Code /mcp panel reads:
# User MCPs (/home/node/.claude/.claude.json) -> server absent
# APM wrote here instead:
$ jq -r '.mcpServers // {} | keys[]' "$HOME/.claude.json"
<server-name>
Additional context
Root cause. The Claude MCP adapter hardcodes the user-scope path.
src/apm_cli/adapters/client/claude.py:
def _user_claude_json_path(self) -> Path:
return Path.home() / ".claude.json" # line 143-144
def get_config_path(self):
if self._is_user_scope():
return str(self._user_claude_json_path()) # line 149-151
return str(self._project_mcp_path())
def _merge_user_mcp(self, config_updates) -> bool:
path = self._user_claude_json_path() # line 192-193
...
No part of the MCP write path reads CLAUDE_CONFIG_DIR. Confirmed with:
$ grep -rn "CLAUDE_CONFIG_DIR" src/apm_cli/adapters/ \
src/apm_cli/integration/mcp_integrator.py src/apm_cli/install/mcp/
# (no matches)
By contrast, the user-scope target resolver already implements the right behavior for every other primitive.
src/apm_cli/integration/targets.py (around line 389):
# Claude Code honors CLAUDE_CONFIG_DIR (default ~/.claude) and Hermes
# honors HERMES_HOME (default ~/.hermes); mirror that at user scope so
# `apm install -g` lands where the tool reads.
if self.name in ("claude", "hermes"):
env_var = "CLAUDE_CONFIG_DIR" if self.name == "claude" else "HERMES_HOME"
env = os.environ.get(env_var, "").strip()
if env:
abs_path = Path(env).expanduser().resolve(strict=False)
home = Path.home().resolve(strict=False)
try:
new_root = abs_path.relative_to(home).as_posix()
except ValueError:
new_root = str(abs_path)
So APM deploys skills, commands, agents, instructions, and hooks under $CLAUDE_CONFIG_DIR, but the MCP servers for the same install land in ~/.claude.json. On any host where those two locations differ, the MCP half of an apm install -g is effectively a no-op from Claude Code's perspective.
Suggested fix. Resolve the user-scope path through CLAUDE_CONFIG_DIR, preserving the current default when it is unset:
import os
def _user_claude_json_path(self) -> Path:
base = os.environ.get("CLAUDE_CONFIG_DIR", "").strip()
if base:
return Path(base).expanduser() / ".claude.json"
return Path.home() / ".claude.json"
Unset CLAUDE_CONFIG_DIR keeps today's behavior (~/.claude.json); set relocates to $CLAUDE_CONFIG_DIR/.claude.json. Ideally this reuses the same resolver already in targets.py so the two sites cannot drift, including the "points outside $HOME" fallback that block already handles.
Describe the bug
At user scope (
apm install -g), APM writes MCP servers for the Claude Code target to a hardcoded~/.claude.jsonand does not consultCLAUDE_CONFIG_DIR. WhenCLAUDE_CONFIG_DIRis set (common in devcontainers and relocated setups), Claude Code reads user-scope MCP servers from$CLAUDE_CONFIG_DIR/.claude.json, so APM's writes land in a file Claude Code never reads. The servers silently fail to appear in/mcp, and because APM re-reads the same hardcoded file on the next run it reports them as "already configured (skipped)", which hides the problem.This is inconsistent with the rest of APM: the user-scope target resolver in
integration/targets.pyalready honorsCLAUDE_CONFIG_DIRfor every other primitive (skills, commands, agents, instructions, hooks). Only the MCP writer diverges.Note on default behavior: with
CLAUDE_CONFIG_DIRunset, Claude Code reads user config from$HOME/.claude.json(home root), so APM's hardcoded path happens to match. The bug only surfaces whenCLAUDE_CONFIG_DIRis set, which relocates the file to$CLAUDE_CONFIG_DIR/.claude.json. That is exactly the scenario the variable exists for (devcontainers, relocated or shared config), so the mismatch is easy to hit and hard to notice.To Reproduce
Steps to reproduce the behavior:
export CLAUDE_CONFIG_DIR="$HOME/.claude"(any dir Claude Code uses). In a Claude Code session,/mcpshows the user MCP source as$CLAUDE_CONFIG_DIR/.claude.json.apm install -g --mcp <some-server>./mcp. The server is absent.jq -r '.mcpServers // {} | keys[]' "$HOME/.claude.json". The server is present in$HOME/.claude.json, not in$CLAUDE_CONFIG_DIR/.claude.json.Expected behavior
MCP servers for the claude target at user scope are written to
$CLAUDE_CONFIG_DIR/.claude.jsonwhenCLAUDE_CONFIG_DIRis set, falling back to~/.claude.jsonwhen it is not. This matches the file Claude Code reads and mirrors the resolution APM already performs for other user-scope primitives. Instead, the servers are written to a file Claude Code never reads, and the "already configured" skip on re-runs makes the mismatch silent.Environment (please complete the following information):
apm --version; source references below are against 0.24.0, commitb915f8d]claude --version]CLAUDE_CONFIG_DIR:$HOME/.claude(set in a devcontainer, so Claude Code reads$HOME/.claude/.claude.json)Logs
APM reports the servers as configured while
/mcpdoes not list them. The two files diverge:Additional context
Root cause. The Claude MCP adapter hardcodes the user-scope path.
src/apm_cli/adapters/client/claude.py:No part of the MCP write path reads
CLAUDE_CONFIG_DIR. Confirmed with:By contrast, the user-scope target resolver already implements the right behavior for every other primitive.
src/apm_cli/integration/targets.py(around line 389):So APM deploys skills, commands, agents, instructions, and hooks under
$CLAUDE_CONFIG_DIR, but the MCP servers for the same install land in~/.claude.json. On any host where those two locations differ, the MCP half of anapm install -gis effectively a no-op from Claude Code's perspective.Suggested fix. Resolve the user-scope path through
CLAUDE_CONFIG_DIR, preserving the current default when it is unset:Unset
CLAUDE_CONFIG_DIRkeeps today's behavior (~/.claude.json); set relocates to$CLAUDE_CONFIG_DIR/.claude.json. Ideally this reuses the same resolver already intargets.pyso the two sites cannot drift, including the "points outside$HOME" fallback that block already handles.