fix(install): respect $CLAUDE_CONFIG_DIR in install/uninstall/update#321
fix(install): respect $CLAUDE_CONFIG_DIR in install/uninstall/update#321halindrome wants to merge 1 commit into
Conversation
Previously, `codebase-memory-mcp install` and `codebase-memory-mcp update`
unconditionally wrote skills, MCP entries, hook scripts, and settings.json
under $HOME/.claude/, even when CLAUDE_CONFIG_DIR pointed at a different
location. Users with non-default Claude Code config dirs ended up with a
new (silently created) ~/.claude/ tree while their real config dir stayed
untouched, so the agent could not see the artifacts the installer claimed
to write.
This commit:
- Adds cbm_claude_config_dir(), cbm_claude_user_root(), and
cbm_resolve_hook_command() helpers in src/cli/cli.c that honor
$CLAUDE_CONFIG_DIR with $HOME/.claude as the legacy fallback.
- Routes install_claude_code_config(), uninstall_claude_code(),
cbm_install_hook_gate_script(), cbm_install_session_reminder_script(),
and cbm_detect_agents()'s claude_code probe through those helpers.
- Replaces the CMM_HOOK_COMMAND / CMM_SESSION_COMMAND macro literals
("~/.claude/hooks/...") with runtime-resolved hook command strings
written into settings.json. The legacy tilde form is preserved when
CLAUDE_CONFIG_DIR is unset so existing settings.json files stay
portable across HOME values.
- Mirrors the change in scripts/setup.sh (settings_file path) and
scripts/security-install.sh (SKILLS_DIR path) so the shell-side
helpers also honor CLAUDE_CONFIG_DIR.
Bundled migration safeguard:
- When install runs with CLAUDE_CONFIG_DIR set and a legacy ~/.claude
directory still exists, install emits a one-line stderr nudge naming
both paths so users can clean up stale skills/hooks/settings without
needing a follow-up release.
Tests:
- Adds cli_detect_agents_finds_claude_via_env to verify CLAUDE_CONFIG_DIR
detection. Updates cli_detect_agents_finds_claude and
cli_detect_agents_none_found to unset/restore CLAUDE_CONFIG_DIR so
the runner's real env does not leak in.
- Adds Phase 9c to scripts/smoke-test.sh covering: skills/.mcp.json/
.claude.json/settings.json/hooks all landing under $CLAUDE_CONFIG_DIR
(not $HOME/.claude), settings.json hook command path, migration
nudge surface, uninstall removal, legacy ~/.claude untouched, and
legacy default preserving the tilde form when CLAUDE_CONFIG_DIR is
unset.
Closes DeusData#320
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
QA Round 1 — PR #321 (
|
| # | What | Status | Evidence / Notes |
|---|---|---|---|
| 1 | Helper trio correct (buffer size handling, NULL guards, env precedence, tilde preserved only when env unset) | PASS | All three helpers (cbm_claude_config_dir, cbm_claude_user_root, cbm_resolve_hook_command) early-return on out_sz==0, zero-init out[0]='\0', branch on cbm_safe_getenv (non-empty wins) then fall back to home_dir. cbm_resolve_hook_command emits ~/.claude/hooks/<name> only when env unset. |
| 2 | Every ~/.claude/... write site routed through helpers |
PASS | Remaining literals in src/cli/cli.c: lines 973/984/988/1017 (helper bodies), 1692 (comment), 2715/2736/3048 (paths derived from home-based legacy_dir/mcp_path2/comment — all correct uses). cli.h lines 67/113/168 are doc comments. No missed callsite. |
| 3 | .claude.json placement: $CCD/.claude.json when env set, else $HOME/.claude.json |
PASS | cbm_claude_user_root returns env-or-home; mcp_path2 is <user_root>/.claude.json in both install_claude_code_config (line 2715) and uninstall_claude_code (line 3048). Manual T1 confirms .claude.json written under $CCD, no stray $HOME/.claude.json. |
| 4 | Hook command in settings.json: tilde when env unset, absolute when env set |
PASS | Manual T1: PreToolUse: $CCD/hooks/cbm-code-discovery-gate; SessionStart: $CCD/hooks/cbm-session-reminder. Manual T2 (env unset): PreToolUse: ~/.claude/hooks/cbm-code-discovery-gate; SessionStart: ~/.claude/hooks/cbm-session-reminder. |
| 5 | Removal symmetry — old ~/.claude/... entries replaced by env-set absolute path |
PASS | is_cmm_hook_entry matches by matcher_str only (line 1525). Manual T5: installed with env unset (1 PreToolUse entry, tilde cmd) → re-installed with CCD set → settings.json still has 1 PreToolUse entry, command rewritten to absolute path. SessionStart: still exactly 4 entries, all rewritten. |
| 6 | Migration nudge condition (CCD set AND legacy exists AND differs); no false positives | PASS | T1 (CCD set, legacy present, differs): nudge fires. T2 (CCD unset): silent. T3 (CCD set, no legacy dir): silent. T4 (CCD == $HOME/.claude): silent (the strcmp(legacy_dir, config_dir) != 0 guard handles the identical-path case). |
| 7 | Test coverage matches PR claims | PASS | New cli_detect_agents_finds_claude_via_env exercises env-set path with no home/.claude. Existing cli_detect_agents_finds_claude and cli_detect_agents_none_found now save/restore CLAUDE_CONFIG_DIR so the runner's real env can't leak. Phase 9c covers 10 placement/nudge/uninstall/symmetry assertions. |
| 8 | Shell layer minimal & correct | PASS | setup.sh swaps the literal for ${CLAUDE_CONFIG_DIR:-$HOME/.claude} plus updates the printed instruction text. security-install.sh swaps SKILLS_DIR. Both use the standard :- fallback idiom. No accidental side effects. |
| 9 | Style — conventional commits, code style consistent, no debug prints, no leftover TODO | PASS | All commits follow fix({scope}) pattern. Helpers follow the file's static-helper / snprintf / CLI_BUF_1K conventions. No printf debug prints. No // TODO. |
| 10 | Backwards compatibility — pre-existing settings.json with ~/.claude/hooks/... upserts cleanly |
PASS | T5 confirmed matcher-based dedup: only one PreToolUse entry survives the env-flip upgrade, with the command rewritten to the new absolute form. |
| 11 | Full test suite | PASS | scripts/test.sh: 2813 passed, 0 failed. |
Findings
Must-fix: None.
Nice-to-have (advisory only):
- Doc comment drift.
src/cli/cli.c:1692andsrc/cli/cli.h:{67,113,168}still describe paths as~/.claude/...even though install location is now CCD-aware. Comments are not load-bearing for behavior, but a one-line update to mention "or$CLAUDE_CONFIG_DIR" would prevent future grep confusion. - Migration nudge wording. The current nudge prints the absolute legacy path (e.g.
legacy /tmp/x/.claude still exists). If the user invoked install from a non-interactive context the absolute path is fine; for interactive use,~/.claudereads more cleanly. Strictly cosmetic. config_dir[0]==0guard divergence.cbm_install_hook_gate_scriptandcbm_install_session_reminder_scriptearly-return whenconfig_dir[0]is empty, butinstall_claude_code_configitself does not checkconfig_dir[0]before derivingskills_dir/mcp_path/settings_path. In practicehome==NULLis already filtered upstream, so this is unreachable, but adding a symmetricif (!config_dir[0]) return;would be defensive.cli.hcbm_detected_agents_t.claude_codecomment still says/* ~/.claude/ exists */; could read/* ~/.claude or $CLAUDE_CONFIG_DIR exists */to match the new detection semantics.
No security or correctness regression detected.
Final verdict
PASS-WITH-ADVISORY — the fix is correct, well-tested, and backwards-compatible. The four advisory items are documentation-comment polish, not behavior bugs. Recommend marking the PR ready for review.
Route Claude Code config paths (skills, .mcp.json, .claude.json, settings.json, hook scripts) and agent detection through CLAUDE_CONFIG_DIR-aware helpers, falling back to ~/.claude. Hook command strings written to settings.json keep the legacy tilde form when the env var is unset, so existing configs stay portable across HOME values. Prints a one-line migration nudge when CLAUDE_CONFIG_DIR is set and a legacy ~/.claude tree still exists. Adds cli_detect_agents_finds_claude_via_env and isolates CLAUDE_CONFIG_DIR in the existing detection tests so the runner env can't leak in. Distilled from #321 onto current main (adapts to the v0.7.0 non-blocking augmenter hook signature, which the original branch predated). Closes #320.
|
Thank you, @halindrome — this is genuinely excellent work. The three-helper design ( Since this branch predated the v0.7.0 hook rework (#288/#362 — the PreToolUse gate became a non-blocking augmenter, which changed Landed in dedd33d ( Closing this PR as the work is now on main. Thank you again for the contribution and the thoroughness — it made the distill trivial. 🙏 |
Route Claude Code config paths (skills, .mcp.json, .claude.json, settings.json, hook scripts) and agent detection through CLAUDE_CONFIG_DIR-aware helpers, falling back to ~/.claude. Hook command strings written to settings.json keep the legacy tilde form when the env var is unset, so existing configs stay portable across HOME values. Prints a one-line migration nudge when CLAUDE_CONFIG_DIR is set and a legacy ~/.claude tree still exists. Adds cli_detect_agents_finds_claude_via_env and isolates CLAUDE_CONFIG_DIR in the existing detection tests so the runner env can't leak in. Distilled from DeusData#321 onto current main (adapts to the v0.7.0 non-blocking augmenter hook signature, which the original branch predated). Closes DeusData#320.
Summary
codebase-memory-mcp install(and thereforeupdate's Step 6 refresh) hardcoded$HOME/.claude/...for skills, MCP entries, hook scripts,.claude.json, andsettings.json. When users hadCLAUDE_CONFIG_DIRset to a non-default location (e.g.~/.config/claude-code, where Claude Code's own config lives), the installer silently created a separate~/.claude/tree that the agent never saw. Hooks installed at runtime already honoredCLAUDE_CONFIG_DIR; only the install/uninstall paths were mismatched.This PR fixes that. It also bundles a migration nudge so the same release that fixes the bug helps users with stale
~/.claude/trees clean them up — explained under "Bundling rationale" below.Closes #320.
What changes
Core helpers in
src/cli/cli.c:cbm_claude_config_dir(home, out, sz)— returns$CLAUDE_CONFIG_DIRor$home/.claude.cbm_claude_user_root(home, out, sz)— returns$CLAUDE_CONFIG_DIRor$home(parent dir for.claude.json, which moves to$CLAUDE_CONFIG_DIR/.claude.jsonwhen the env var is set, matching Claude Code's own behavior).cbm_resolve_hook_command(script_name, out, sz)— produces the hookcommandstring written intosettings.json. WhenCLAUDE_CONFIG_DIRis unset, it preserves the legacy~/.claude/hooks/<script>tilde form so existingsettings.jsonfiles stay portable acrossHOMEvalues.Call sites routed through the helpers:
install_claude_code_config— skills,.mcp.json,.claude.json,settings.json, hook script installuninstall_claude_code— same set, mirroredcbm_install_hook_gate_script,cbm_install_session_reminder_script— hook script destinationcbm_detect_agents—agents.claude_codenow true when either~/.claudeor$CLAUDE_CONFIG_DIRexistsThe
CMM_HOOK_COMMAND/CMM_SESSION_COMMANDmacros (which baked~/.claude/hooks/...into settings.json) are replaced byCMM_HOOK_GATE_SCRIPT/CMM_SESSION_REMINDER_SCRIPT(basenames), with the full path resolved at install time viacbm_resolve_hook_command. Removal still works becauseis_cmm_hook_entrymatches bymatcher_str, not by command path.Shell layer:
scripts/setup.sh—configure_claudenow uses${CLAUDE_CONFIG_DIR:-$HOME/.claude}forsettings_fileand the printed instructions.scripts/security-install.sh—SKILLS_DIRlikewise.Bundling rationale
This PR rolls out the fix and the migration nudge together rather than splitting them across two releases:
CLAUDE_CONFIG_DIRset would see their config silently migrate to the env-pointed dir. Their old~/.claude/{skills,hooks,settings.json,.mcp.json}would still be on disk — orphaned, never written again, easy to forget.installwhen (a)CLAUDE_CONFIG_DIRis set, and (b) a legacy~/.claudedirectory still exists. It names both paths so users can choose to clean up.The nudge is non-fatal, prints once per
installinvocation, and is silent in the legacy-default case (CLAUDE_CONFIG_DIRunset → no divergence to nudge about).Tests
tests/test_cli.c: newcli_detect_agents_finds_claude_via_envasserts thatcbm_detect_agentsreportsclaude_code=truewhenCLAUDE_CONFIG_DIRpoints at an existing dir outsidehome_dir/.claude.cli_detect_agents_finds_claudeandcli_detect_agents_none_foundnow unset/restoreCLAUDE_CONFIG_DIRso the runner's real env does not leak in. All 2813 unit tests pass.scripts/smoke-test.shPhase 9c: end-to-end install/uninstall underCLAUDE_CONFIG_DIR=$FAKE_HOME/custom-claudewith a pre-existing$FAKE_HOME/.claude. Asserts:skills/,settings.json,.mcp.json,.claude.json,hooks/cbm-code-discovery-gateall land under$CLAUDE_CONFIG_DIR.$HOME/.claude(regression guard).settings.jsonis the absolute$CLAUDE_CONFIG_DIR/hooks/...path, not~/.claude/hooks/....$CLAUDE_CONFIG_DIRand leaves legacy$HOME/.claudeuntouched.CLAUDE_CONFIG_DIRunset,settings.jsonkeeps the~/.claude/hooks/...tilde form (legacy default preserved).install+uninstallcycle with both env-set and env-unset configs, confirmed file placement and migration nudge surface match expectations.Out of scope (intentionally separate)
--scope=project): a real feature gap, but a separate change. Not bundled.EXPECTED_PATTERNSallow-list inscripts/security-install.shis unchanged;.config/...already matches typicalCLAUDE_CONFIG_DIRsetups, and broadening it without a concrete test is not worth the security audit risk.Test plan
scripts/build.shscripts/test.sh(2813 passed)install/uninstallwithCLAUDE_CONFIG_DIRsetinstall/uninstallwithCLAUDE_CONFIG_DIRunset (legacy default)_lint.yml(clang-format-20)_build.yml🤖 Generated with Claude Code