fix(cli): honour CLAUDE_CONFIG_DIR when resolving Claude hook settings - #1420
fix(cli): honour CLAUDE_CONFIG_DIR when resolving Claude hook settings#1420fzipi wants to merge 2 commits into
Conversation
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>
acdb2ad to
c189352
Compare
There was a problem hiding this comment.
💡 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".
| if project != Path.home() and project_dir != user_dir: | ||
| sources.extend((project_dir / "settings.json", project_dir / "settings.local.json")) |
There was a problem hiding this comment.
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 👍 / 👎.
There was a problem hiding this comment.
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>
Fixes #1418.
What this does
_claude_user_dir()resolves the user-level Claude config directory fromCLAUDE_CONFIG_DIR, falling back to~/.claudewhen it is unset or blank. Both places that previously hardcodedPath.home() / ".claude"now use it:load_claude_settings()— the user-levelbasicMemorybase._hook_config_path()— thehook install/hook removetarget.Claude Code treats
CLAUDE_CONFIG_DIRas a full replacement for~/.claude, so with per-account profiles the old behaviour meant a profile'sbasicMemoryblock was never read (hook statusprintedsettings: not found, capture fell back to the default project), andhook installwrote hook entries into a different profile's settings file.One subtlety worth reviewing
_claude_project_dir()walks ancestors, and that walk can reach$HOMEand match~/.claude/settings.json. The old code suppressed that withif project != home. Comparing only the new profile dir is not enough — withCLAUDE_CONFIG_DIRpointing elsewhere,~/.claudeno longer equals the user dir, so it re-enters as a project-level source and outranks the active profile. Both guards are kept:test_claude_config_dir_ignores_default_home_settingscovers 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 retainedproject != 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 installwrites into the profile dir and leaves~/.claudeuntouched;hook removecleans up there.tests/cli/conftest.py::isolated_homenow clearsCLAUDE_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/clipasses in full — 937 tests.ruff check,ruff format --check, andty check src tests test-intare clean; the 4pymilvusunresolved-import diagnostics are pre-existing onmainand unrelated.Note on Codex
~/.codexis left alone.CODEX_HOMEwould be the equivalent knob, but that is a separate change and I did not want to widen this one.🤖 Generated with Claude Code