Skip to content

fix(cli): honour CLAUDE_CONFIG_DIR when resolving Claude hook settings - #1420

Open
fzipi wants to merge 2 commits into
basicmachines-co:mainfrom
fzipi:fix/claude-config-dir
Open

fix(cli): honour CLAUDE_CONFIG_DIR when resolving Claude hook settings#1420
fzipi wants to merge 2 commits into
basicmachines-co:mainfrom
fzipi:fix/claude-config-dir

Conversation

@fzipi

@fzipi fzipi commented Aug 31, 2026

Copy link
Copy Markdown

Fixes #1418.

What this does

_claude_user_dir() resolves the user-level Claude config directory from CLAUDE_CONFIG_DIR, falling back to ~/.claude when it is unset or blank. Both places that previously hardcoded Path.home() / ".claude" now use it:

  • load_claude_settings() — the user-level basicMemory base.
  • _hook_config_path() — the hook install / hook remove target.

Claude Code treats CLAUDE_CONFIG_DIR as a full replacement for ~/.claude, so with per-account profiles the old behaviour meant a profile's basicMemory block was never read (hook status printed settings: not found, capture fell back to the default project), and hook install wrote hook entries into a different profile's settings file.

One subtlety worth reviewing

_claude_project_dir() walks ancestors, and that walk can reach $HOME and match ~/.claude/settings.json. The old code suppressed that with if project != home. Comparing only the new profile dir is not enough — with CLAUDE_CONFIG_DIR pointing elsewhere, ~/.claude no longer equals the user dir, so it re-enters as a project-level source and outranks the active profile. Both guards are kept:

if project != Path.home() and project_dir != user_dir:

test_claude_config_dir_ignores_default_home_settings covers exactly this; it fails with only the profile-dir comparison in place.

Behaviour when CLAUDE_CONFIG_DIR is unset

Unchanged. _claude_user_dir() returns ~/.claude, and the retained project != Path.home() guard preserves the original precedence, including the malformed-file fail-closed path.

Tests

Seven added in tests/cli/test_hook_command.py: profile settings are read; the default profile's settings do not leak in; project settings still win over a profile block; blank and whitespace values fall back to ~/.claude; ~ is expanded; hook install writes into the profile dir and leaves ~/.claude untouched; hook remove cleans up there.

tests/cli/conftest.py::isolated_home now clears CLAUDE_CONFIG_DIR, so a contributor running the suite under a profile wrapper does not have tests read their real config.

Verified the new tests fail without the source change (6 of 8 fail; the two blank-value cases assert fallback behaviour that already held). tests/cli passes in full — 937 tests. ruff check, ruff format --check, and ty check src tests test-int are clean; the 4 pymilvus unresolved-import diagnostics are pre-existing on main and unrelated.

Note on Codex

~/.codex is left alone. CODEX_HOME would be the equivalent knob, but that is a separate change and I did not want to widen this one.

🤖 Generated with Claude Code

@CLAassistant

CLAassistant commented Aug 31, 2026

Copy link
Copy Markdown

CLA assistant check
All committers have signed the CLA.

The Claude hook resolved user-level settings as a hardcoded ~/.claude and
never read CLAUDE_CONFIG_DIR, which Claude Code treats as a full replacement
for that directory. Users running per-account profiles hit two problems: a
basicMemory block in the active profile was never read, so hook status
reported "settings: not found" and capture fell back to the default project;
and `hook install` wrote hook entries into ~/.claude/settings.json regardless
of the active profile, editing another account's configuration.

Add _claude_user_dir(), honouring CLAUDE_CONFIG_DIR and falling back to
~/.claude so single-profile setups are unchanged, and use it for both the
settings base and the install/remove target.

The ancestor walk in _claude_project_dir can reach $HOME and find
~/.claude/settings.json. That was previously suppressed by comparing the
resolved project root to $HOME; keep that guard alongside the new profile-dir
comparison, otherwise the default profile's settings re-enter as a
higher-precedence project source and override the active profile.

Clear CLAUDE_CONFIG_DIR in the CLI test isolation fixture so a contributor
running the suite under a profile wrapper does not read their real config.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Signed-off-by: Felipe Zipitria <fzipi@fing.edu.uy>
@fzipi
fzipi force-pushed the fix/claude-config-dir branch from acdb2ad to c189352 Compare August 31, 2026 22:11

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: acdb2ad346

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment thread src/basic_memory/cli/commands/hook.py Outdated
Comment on lines +276 to +277
if project != Path.home() and project_dir != user_dir:
sources.extend((project_dir / "settings.json", project_dir / "settings.local.json"))

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Keep local overrides when the profile overlaps the project

When CLAUDE_CONFIG_DIR points to the current project's .claude directory—a valid way to create a repo-specific Claude profile—project_dir == user_dir makes this branch skip both project sources. Although deduplicating settings.json is appropriate, the distinct settings.local.json must still be merged; otherwise its higher-precedence primaryProject or capture settings are ignored and hooks can route notes to the profile-wide project instead. Deduplicate the individual settings.json path while retaining settings.local.json.

AGENTS.md reference: AGENTS.md:L393-L393

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Good catch — confirmed and fixed in 54df21f.

I reproduced it before changing anything: with CLAUDE_CONFIG_DIR set to the project's own .claude directory, settings.local.json was dropped entirely and primaryProject fell back to the profile-wide value.

The fix deduplicates by resolved file path rather than by directory, so settings.json is read once as the user-level source while settings.local.json still layers on top:

if project != Path.home():
    project_dir = project / ".claude"
    seen = {path.resolve() for path in sources}
    for name in ("settings.json", "settings.local.json"):
        path = project_dir / name
        if path.resolve() not in seen:
            sources.append(path)

resolve() matters on macOS, where the project path is already resolved but the env value may not be.

Covered by test_claude_config_dir_pointing_at_project_keeps_local_override, which fails without the change. The project != Path.home() guard is kept, so ~/.claude/settings.local.json is still not read as a project source when the ancestor walk reaches $HOME — that is pre-existing behaviour and out of scope here.

One note: the cited AGENTS.md:L393 is a glossary entry about knowledge structure and does not relate to settings precedence.

CLAUDE_CONFIG_DIR can legitimately point at a repository's own .claude
directory. Skipping the project sources whenever that directory matched the
profile dir dropped settings.local.json entirely, so its higher-precedence
primaryProject was ignored and hooks routed notes to the profile-wide project.

Deduplicate by resolved file path rather than by directory: settings.json is
read once as the user-level source, and settings.local.json still layers on
top of it.

Reported by the Codex review bot on basicmachines-co#1420.

Signed-off-by: Felipe Zipitria <fzipi@fing.edu.uy>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

2 participants