WEB-4882: honor CLAUDE_CONFIG_DIR in Claude Code hooks install#175
WEB-4882: honor CLAUDE_CONFIG_DIR in Claude Code hooks install#175MohamedAklamaash wants to merge 12 commits into
Conversation
Resolve the Claude config dir from --config-dir / CLAUDE_CONFIG_DIR (fallback ~/.claude) in setup.py and at hook runtime in unbound.py, so hooks, settings, the baked command path, audit log, cache, and backfill transcripts all live where Claude reads them when a custom dir is set. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
🛡️ Automated Security Review (consensus)3 findings — 3 high-confidence, 0 to triage. Reviewers: Cursor, Claude, Semgrep, Gitleaks. 🔴 HIGH — Installer/runtime config-dir resolution mismatch
Impact: Fix: Bake the resolved config dir into the hook invocation (e.g. pass Flagged by: Cursor, Claude 🟡 MEDIUM — Whitespace-only
|
- unbound.py: resolve the config dir from the hook's own install location (__file__), so runtime paths always match where the installer wrote them, regardless of how CLAUDE_CONFIG_DIR is propagated into the hook env. - setup.py: strip whitespace-only CLAUDE_CONFIG_DIR, and don't let --config-dir swallow a following flag as its value. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
|
Addressed the review feedback in
|
….json - unbound.py: resolve _CONFIG_DIR from CLAUDE_CONFIG_DIR (stripped) again, not __file__. Deriving from __file__ made SELF_SCRIPT_PATH always equal the running script, defeating the MDM self-update guard that must skip admin-managed installs. Strip the env value so whitespace-only falls back to ~/.claude consistently for both _CONFIG_DIR and _config_dir_is_default. - unbound.py: CLAUDE_MCP_CONFIG_PATH probes $CONFIG_DIR/.claude.json and falls back to ~/.claude.json, so account-identity reads never break if Claude keeps the OAuth config at the home sibling. - setup.py: strip the --config-dir value too, matching the env handling. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
|
Thanks — the re-review caught that my
On the original [High] install-arg / runtime-env mismatch: in the real flow this can't diverge — the CLI computes
|
🛡️ Automated Security Review (consensus)0 findings — 0 high-confidence, 0 to triage. Reviewers: Cursor, Claude, Semgrep, Gitleaks. ✅ Security consensus: no issues found. (reviewers: Cursor, Claude, Semgrep, Gitleaks) Previously acknowledged (not re-flagged)
🤖 consensus review · reviewers: Cursor,Claude,Semgrep,Gitleaks · head |
Make setup.py prioritize CLAUDE_CONFIG_DIR (env) over --config-dir, with the CLI arg as fallback. unbound.py resolves runtime paths from the same env, so install-time placement and runtime resolution now agree by the same precedence instead of diverging. The CLI passes --config-dir derived from CLAUDE_CONFIG_DIR, so the gated real flow is unchanged. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
|
[High] Hook config dir ignores CLI arg — fixed in |
🛡️ Automated Security Review (consensus)0 findings — 0 high-confidence, 0 to triage. Reviewers: Cursor, Claude, Semgrep, Gitleaks. ✅ Security consensus: no issues found. (reviewers: Cursor, Claude, Semgrep, Gitleaks) Previously acknowledged (not re-flagged)
🤖 consensus review · reviewers: Cursor,Claude,Semgrep,Gitleaks · head |
|
[High] Clear misses custom config directory — by design, with the same precedence as install. The narrow gap is purely an ad-hoc-env one (set |
…aude-config-dir # Conflicts: # claude-code/hooks/setup.py # claude-code/hooks/test_setup.py # claude-code/hooks/unbound.py
| def _resolve_claude_config_dir(argv) -> Path: | ||
| value = (os.environ.get("CLAUDE_CONFIG_DIR") or "").strip() or None | ||
| if not value: | ||
| for i, arg in enumerate(argv): | ||
| if arg == "--config-dir" and i + 1 < len(argv) and not argv[i + 1].startswith("--"): | ||
| value = argv[i + 1].strip() or None | ||
| break | ||
| if not value: | ||
| return Path.home() / ".claude" | ||
| return Path(value).expanduser().resolve() |
There was a problem hiding this comment.
Env-var beats CLI arg — precedence is inverted from stated intent
_resolve_claude_config_dir checks CLAUDE_CONFIG_DIR first, then falls through to --config-dir. The "Changes" section of the PR description explicitly states the intended order is --config-dir arg > CLAUDE_CONFIG_DIR env > ~/.claude. The implementation (and its test test_env_beats_arg_and_home) does the opposite. With this order, any user who has CLAUDE_CONFIG_DIR set in their shell profile cannot override the installation target via --config-dir, which is the standard CLI convention.
| def _resolve_claude_config_dir(argv) -> Path: | |
| value = (os.environ.get("CLAUDE_CONFIG_DIR") or "").strip() or None | |
| if not value: | |
| for i, arg in enumerate(argv): | |
| if arg == "--config-dir" and i + 1 < len(argv) and not argv[i + 1].startswith("--"): | |
| value = argv[i + 1].strip() or None | |
| break | |
| if not value: | |
| return Path.home() / ".claude" | |
| return Path(value).expanduser().resolve() | |
| def _resolve_claude_config_dir(argv) -> Path: | |
| # CLI arg is the most specific — check it first. | |
| for i, arg in enumerate(argv): | |
| if arg == "--config-dir" and i + 1 < len(argv) and not argv[i + 1].startswith("--"): | |
| value = argv[i + 1].strip() or None | |
| if value: | |
| return Path(value).expanduser().resolve() | |
| break | |
| # Fall back to environment variable. | |
| value = (os.environ.get("CLAUDE_CONFIG_DIR") or "").strip() or None | |
| if value: | |
| return Path(value).expanduser().resolve() | |
| return Path.home() / ".claude" |
vigneshsubbiah16
left a comment
There was a problem hiding this comment.
🛡️ Automated Security Review (consensus)
0 findings — 0 high-confidence, 0 to triage. Reviewers: Cursor, Claude, Semgrep, Gitleaks.
✅ Security consensus: no issues found. (reviewers: Cursor, Claude, Semgrep, Gitleaks)
Previously acknowledged (not re-flagged)
- Install
--config-dirvsCLAUDE_CONFIG_DIR/ runtime mismatch — env-first precedence is deliberate; CLI and Claude runtime both derive fromCLAUDE_CONFIG_DIR, so install and hook paths align in the normal flow. - MDM self-update guard —
__file__-based_CONFIG_DIRwas reverted; env-based resolution restores the managed-install skip for__file__ != SELF_SCRIPT_PATH. .claude.jsonlocation under custom config dir — intentional: probe$CONFIG_DIR/.claude.json, fall back to~/.claude.json.--clear/--backfillwithout ad-hoc env — accepted by design; same resolution as install; persistentCLAUDE_CONFIG_DIRis required (follow-up offered for home-anchored state).- Whitespace-only
CLAUDE_CONFIG_DIR/--config-dir, flag-as-value parsing — fixed via.strip()andnot argv[i+1].startswith("--"). - Semgrep
insecure-file-permissions(0o700/0o755) — pre-existing chmod patterns; 0o700 is owner-only (stricter than 0o644); not introduced by this diff. - Semgrep
dynamic-urllib-usein tests — test-only mock URL fetch; out of scope for this path-threading change.
🤖 consensus review · reviewers: Cursor,Claude,Semgrep,Gitleaks · head d74ba52a · 2026-06-26T06:11Z
🛡️ Automated Security Review (consensus)0 findings — 0 high-confidence, 0 to triage. Reviewers: Cursor, Claude, Semgrep, Gitleaks. ✅ Security consensus: no issues found. (reviewers: Cursor, Claude, Semgrep, Gitleaks) Previously acknowledged (not re-flagged)
🤖 consensus review · reviewers: Cursor,Claude,Semgrep,Gitleaks · head |
Mirror the hooks installer: resolve the config dir from CLAUDE_CONFIG_DIR / the --config-dir arg the CLI forwards (else ~/.claude), and thread it through the key-helper writer, settings, install-state detection, and clear. apiKeyHelper keeps the portable ~/.claude form for the default dir and uses the absolute path when relocated so Claude resolves it under the active dir. Adds gateway test_setup. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
🛡️ Automated Security Review (consensus)1 finding — 0 high-confidence, 1 to triage. Reviewers: Cursor, Claude, Semgrep, Gitleaks. 🟡 TRIAGE — Symlinked config dir breaks hook detection on clear/reinstall
Previously acknowledged (not re-flagged)
🤖 consensus review · reviewers: Cursor,Claude,Semgrep,Gitleaks · head |
Strip WEB-4882 references and redundant inline comments; behavior unchanged.
🛡️ Automated Security Review (consensus)0 findings — 0 high-confidence, 0 to triage. Reviewers: Cursor, Claude, Semgrep, Gitleaks. ✅ Security consensus: no issues found. (reviewers: Cursor, Claude, Semgrep, Gitleaks) Previously acknowledged (not re-flagged)
🤖 consensus review · reviewers: Cursor,Claude,Semgrep,Gitleaks · head |
…lear sweep, portable apiKeyHelper - unbound.py: resolve .claude.json AND plugins/cache the same way — prefer the relocated dir when it has the artifact, else the legacy ~/.claude location — so MCP/plugin policy reads from wherever Claude actually stores them. - setup --clear (hooks + gateway): when the config dir is relocated, also strip enforcement left behind in the default ~/.claude so nothing fires if Claude later runs without CLAUDE_CONFIG_DIR. - gateway apiKeyHelper: compare resolved paths so the portable ~/.claude form is kept even on symlinked HOME / when CLAUDE_CONFIG_DIR equals the default dir. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
vigneshsubbiah16
left a comment
There was a problem hiding this comment.
🛡️ Automated Security Review (consensus)
0 findings — 0 high-confidence, 0 to triage. Reviewers: Cursor, Claude, Semgrep, Gitleaks.
✅ Security consensus: no issues found. (reviewers: Cursor, Claude, Semgrep, Gitleaks)
Previously acknowledged (not re-flagged)
.claude.json/ MCP config path at import (unbound.py:29) — maintainer: deliberate_relocated_or_legacyprobe ($CONFIG_DIR/.claude.jsonthen~/.claude.json); residual wrong-account risk if the relocated file is absent at import is an accepted design choice pending empirical validation of Claude Code’s relocated layout.--clear/--backfillwithoutCLAUDE_CONFIG_DIR(setup.py) — maintainer: by design; custom-dir operations require the same env Claude uses for that profile; ad-hoc-env-only installs are a known narrow gap, with persisted state deferred as follow-up.- Install vs runtime config-dir precedence (env >
--config-dir) — maintainer: intentional so installer andunbound.pyshare one resolution order; CLI forwards env in the normal flow; hand-invokingsetup.pywith a mismatched env is out of scope. - Semgrep
insecure-file-permissions/ testurllibhits — pre-existing installer patterns (0o700/0o755for hook dirs and executables); not introduced or worsened by this path-resolution change.
🤖 consensus review · reviewers: Cursor,Claude,Semgrep,Gitleaks · head 870240e6 · 2026-06-29T12:31Z
🛡️ Automated Security Review (consensus)0 findings — 0 high-confidence, 0 to triage. Reviewers: Cursor, Claude, Semgrep, Gitleaks. ✅ Security consensus: no issues found. (reviewers: Cursor, Claude, Semgrep, Gitleaks) Previously acknowledged (not re-flagged)
🤖 consensus review · reviewers: Cursor,Claude,Semgrep,Gitleaks · head |
# Conflicts: # claude-code/hooks/unbound.py
vigneshsubbiah16
left a comment
There was a problem hiding this comment.
🛡️ Automated Security Review (consensus)
3 findings — 2 high-confidence, 1 to triage. Reviewers: Cursor, Claude, Semgrep, Gitleaks.
🔴 HIGH — Unquoted baked hook / apiKeyHelper paths break on spaces or shell metacharacters
claude-code/hooks/setup.py:429, claude-code/gateway/setup.py:343
Impact: For a custom config dir, the baked hook command and apiKeyHelper are written as raw str(path) on POSIX (Windows quotes only); a dir like /home/user/my configs/cc word-splits at shell launch and Claude Code fails open on hook errors — silent loss of PreToolUse policy enforcement and telemetry.
Fix: Wrap baked paths with shlex.quote() on all platforms (mirror the Windows branch), and update _command_targets_hook matching and test assertions accordingly.
Reviewers: Claude
🔴 HIGH — --config-dir=/path (equals form) silently ignored in hooks installer
claude-code/hooks/setup.py:69
Impact: The manual argv scan only handles --config-dir <value> as two tokens; --config-dir=/path falls through to env/default, so install/clear/backfill can target ~/.claude while Claude runs a custom dir — the same silent policy/telemetry loss WEB-4882 fixes (gateway's argparse path already accepts the equals form).
Fix: Also match arg.startswith("--config-dir=") and split on the first =.
Reviewers: Cursor, Claude
🟡 TRIAGE — Legacy ~/.claude sweep on clear omits auxiliary hook artifacts
claude-code/hooks/setup.py:674
Impact: When clearing a relocated config dir, the legacy sweep removes unbound.py and settings hook entries under ~/.claude but not unbound-setup.py or .last_updated, leaving stale files that could confuse reinstall or state detection.
Fix: Extend the legacy sweep to delete the same extras cleared on the primary config dir.
Reviewers: Cursor
Previously acknowledged (not re-flagged)
- Install/clear without
CLAUDE_CONFIG_DIRin a later shell — by design: clear/backfill use the same env-first resolution as install; persistent env is the supported contract; home-anchored state is a possible follow-up. .claude.json/ OAuth identity via_relocated_or_legacyfallback to~/.claude.json— deliberate design choice pending empirical confirmation of Claude Code's relocation behavior.- Install-time vs runtime config-dir precedence / env-over-arg — intentional (
CLAUDE_CONFIG_DIRfirst so install andunbound.pyagree; CLI derives--config-dirfrom env in the normal flow). - Symlinked config dir breaks
_command_targets_hookmatching on clear — prior consensus triaged to no-issue.
🤖 consensus review · reviewers: Cursor,Claude,Semgrep,Gitleaks · head 1f0f74b9 · 2026-07-02T16:04Z
# Conflicts: # claude-code/gateway/setup.py # claude-code/hooks/setup.py
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes using default effort and found 1 potential issue.
❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.
Reviewed by Cursor Bugbot for commit d83b1b2. Configure here.
| CLAUDE_PLUGIN_CACHE_DIR = Path.home() / ".claude" / "plugins" / "cache" | ||
| POLICY_CACHE_FILE = Path.home() / ".claude" / "hooks" / ".policy_cache.json" | ||
| CLAUDE_MCP_CONFIG_PATH = _relocated_or_legacy(_CONFIG_DIR / ".claude.json", Path.home() / ".claude.json") | ||
| CLAUDE_PLUGIN_CACHE_DIR = _relocated_or_legacy(_CONFIG_DIR / "plugins" / "cache", Path.home() / ".claude" / "plugins" / "cache") |
There was a problem hiding this comment.
Empty cache dir overrides legacy
Medium Severity
_relocated_or_legacy picks the relocated plugin cache whenever that directory exists. With a custom CLAUDE_CONFIG_DIR, an empty plugins/cache under the new tree can win over a populated legacy ~/.claude/plugins/cache, so _resolve_plugin_mcp_config scans an empty dir and MCP plugin policy resolution fails until cache is repopulated.
Reviewed by Cursor Bugbot for commit d83b1b2. Configure here.
vigneshsubbiah16
left a comment
There was a problem hiding this comment.
🛡️ Automated Security Review (consensus)
4 findings — 2 high-confidence, 2 to triage. Reviewers: Cursor, Claude, Semgrep, Gitleaks.
🟡 LOW — --config-dir=/path (equals form) silently ignored in hooks installer
claude-code/hooks/setup.py:66 · 🔴 HIGH
Impact: Callers using --config-dir=/path with no CLAUDE_CONFIG_DIR get hooks/settings under ~/.claude while Claude reads the custom dir — reproducing the silent policy/telemetry loss this PR fixes. Gateway's argparse path accepts both forms.
Fix: Also match arg.startswith("--config-dir=") and split on the first =.
Reviewers: Cursor, Claude
🟡 LOW — Symlinked config dir breaks hook detection on --clear
claude-code':410 · 🔴 HIGH
Impact: _resolve_claude_config_dir returns a resolved path, but _command_targets_hook compares with normpath only; a lexical hook command in an existing settings.json no longer matches, so --clear can leave enforcement active (and reinstall can duplicate entries).
Fix: Compare Path(tokens[0]).resolve() against target.resolve() (fall back to normpath on resolution failure).
Reviewers: Cursor, Claude
🟡 LOW — Legacy ~/.claude sweep removes unrelated apiKeyHelper
claude-code/gateway/setup.py:494 · 🟡 TRIAGE
Impact: When clearing a relocated install, remove_api_key_helper_setting(default_dir) strips apiKeyHelper from ~/.claude/settings.json unconditionally — potentially breaking a user's own or another tool's key-helper config that was never installed by Unbound.
Fix: In the legacy sweep, only remove apiKeyHelper when its value matches Unbound's anthropic_key.sh path.
Reviewers: Claude
🟡 LOW — Legacy ~/.claude sweep omits hook helper files
claude-code/hooks/setup.py:674 · 🟡 TRIAGE
Impact: The relocated-dir legacy sweep removes unbound.py and hook settings entries under ~/.claude, but not unbound-setup.py or .last_updated, leaving stale artifacts after --clear.
Fix: Extend the legacy sweep to delete the same hook extras as the primary clear path.
Reviewers: Cursor
Previously acknowledged (not re-flagged)
- Install-time
--config-dirvs runtimeCLAUDE_CONFIG_DIRdivergence — accepted by design: env-first precedence; CLI derives--config-dirfromCLAUDE_CONFIG_DIR; runtime must stay env-based to preserve the MDM self-update guard. --clear/--backfillwithoutCLAUDE_CONFIG_DIRmissing a custom-dir install — accepted by design: same precedence as install; persistent env is the supported contract; home-anchored install state deferred as follow-up..claude.json/ plugin-cache_relocated_or_legacyexists-probe fallback — deliberate design choice pending empirical verification of Claude Code's relocated layout; residual wrong-identity risk if the relocated file appears only after import is a known/accepted trade-off.
🤖 consensus review · reviewers: Cursor,Claude,Semgrep,Gitleaks · head d83b1b2f · 2026-07-03T07:22Z


WEB-4882 — honor
CLAUDE_CONFIG_DIRin Claude Code hooks installThe installer (
setup.py) and the runtime hook (unbound.py) hardcoded~/.claude. With a customCLAUDE_CONFIG_DIR, hooks were written/read where Claude Code never looks → silent loss of policy enforcement + telemetry.Changes
claude-code/hooks/setup.py:_resolve_claude_config_dir(argv)(precedence--config-dirarg >CLAUDE_CONFIG_DIRenv >~/.claude,expanduser().resolve()); the resolved dir is threaded into hooks dir,settings.json, the baked absolute hook command, uninstall/clear, install-state, and--backfilltranscript discovery.claude-code/hooks/unbound.py: module-level_CONFIG_DIRfromCLAUDE_CONFIG_DIRat import; audit log, error log, policy cache, approval marker, and self-update target resolve from it..claude.jsonuses the custom dir when the env var is set, else the~/.claude.jsonsibling.Notes
CLAUDE_CONFIG_DIR, every path equals~/.claudeexactly; the self-update__file__ == SELF_SCRIPT_PATHguard is unaffected for existing installs.~/.unbound/...paths stay home-anchored (independent of the Claude config dir).--config-dir).Tests
python3 -m pytest test_setup.py -q→ 26 passed. Added precedence, install-under-resolved-dir (asserts the baked command is absolute under the dir), backward-compat, and custom-dir backfill tests.🤖 Generated with Claude Code
Note
Medium Risk
Changes where policy hooks and API-key helpers are written and read; default
~/.claudebehavior is preserved when the env var is unset, but mis-resolution could still break enforcement for relocated installs.Overview
Fixes silent policy/telemetry gaps when Claude Code uses a custom config directory by routing all Unbound install and runtime artifacts through a resolved config dir instead of hardcoded
~/.claude.Gateway and hooks installers add
_resolve_claude_config_dir(CLAUDE_CONFIG_DIR→--config-dir→ default), thread that path through hook/key-helper writes,settings.json, install-state,--backfilltranscript discovery, and--clear. Clear also strips leftover enforcement under~/.claudewhen the active dir is relocated. Gateway keeps portable~/.claude/anthropic_key.shinapiKeyHelperonly for the default dir; custom dirs get an absolute helper path.unbound.pyresolves_CONFIG_DIRfromCLAUDE_CONFIG_DIRat import (logs, policy cache, approval marker, self-update target) and uses_relocated_or_legacyfor.claude.json/ plugin cache when Claude’s layout varies by version.New and updated unit tests cover resolver precedence, install paths, legacy sweep on clear, and custom-dir backfill.
Reviewed by Cursor Bugbot for commit d83b1b2. Bugbot is set up for automated code reviews on this repo. Configure here.
Greptile Summary
This PR fixes silent loss of Unbound policy enforcement when Claude Code uses a non-default config directory (
CLAUDE_CONFIG_DIR) by routing all installer and runtime hook paths through a resolved config dir instead of the hardcoded~/.claude. The change is backward compatible: with noCLAUDE_CONFIG_DIRset every path equals~/.claudeexactly.hooks/setup.pyandgateway/setup.py: new_resolve_claude_config_dir(env > arg >~/.claude) threadsconfig_dirthrough hook install,settings.json,apiKeyHelper,--clear, install-state detection, and--backfilltranscript discovery.hooks/unbound.py: module-level_CONFIG_DIRfromCLAUDE_CONFIG_DIRat import drives all runtime paths;_relocated_or_legacydefers.claude.json/ plugin-cache location to an existence check.Confidence Score: 5/5
Safe to merge — behavior is unchanged when CLAUDE_CONFIG_DIR is unset, and the custom-dir path is exercised by the new tests.
All changed code paths are additive (new config-dir resolution layered on top of unchanged defaults), tests verify both the custom-dir and backward-compat cases, and there are no data-loss or policy-enforcement regressions on the default install path.
claude-code/hooks/setup.py — the manual argv parser in _resolve_claude_config_dir silently ignores the --config-dir=value form.
Important Files Changed
Flowchart
%%{init: {'theme': 'neutral'}}%% flowchart TD A([Start: resolve config dir]) --> B{CLAUDE_CONFIG_DIR set and non-blank?} B -- Yes --> C[Use CLAUDE_CONFIG_DIR .expanduser.resolve] B -- No --> D{--config-dir arg provided?} D -- Yes --> E[Use --config-dir value .expanduser.resolve] D -- No --> F[Default: ~/.claude] C --> G([Resolved config_dir]) E --> G F --> G G --> H[hooks dir = config_dir/hooks] G --> I[settings.json = config_dir/settings.json] G --> J[unbound.py = config_dir/hooks/unbound.py] H & I & J --> L([Install hooks + settings])%%{init: {'theme': 'base', 'themeVariables': {"darkMode": true, "background": "#0d1117", "primaryColor": "#21262d", "primaryTextColor": "#e6edf3", "primaryBorderColor": "#8b949e", "lineColor": "#8b949e", "textColor": "#e6edf3", "edgeLabelBackground": "#161b22", "actorBkg": "#21262d", "actorBorder": "#8b949e", "actorTextColor": "#e6edf3", "actorLineColor": "#8b949e", "signalColor": "#8b949e", "signalTextColor": "#e6edf3", "noteBkgColor": "#373320", "noteBorderColor": "#d4a72c", "noteTextColor": "#f0e6c0", "labelBoxBkgColor": "#21262d", "labelBoxBorderColor": "#8b949e", "labelTextColor": "#e6edf3", "loopTextColor": "#e6edf3", "activationBkgColor": "#30363d", "activationBorderColor": "#8b949e"}}}%% flowchart TD A([Start: resolve config dir]) --> B{CLAUDE_CONFIG_DIR set and non-blank?} B -- Yes --> C[Use CLAUDE_CONFIG_DIR .expanduser.resolve] B -- No --> D{--config-dir arg provided?} D -- Yes --> E[Use --config-dir value .expanduser.resolve] D -- No --> F[Default: ~/.claude] C --> G([Resolved config_dir]) E --> G F --> G G --> H[hooks dir = config_dir/hooks] G --> I[settings.json = config_dir/settings.json] G --> J[unbound.py = config_dir/hooks/unbound.py] H & I & J --> L([Install hooks + settings])Reviews (12): Last reviewed commit: "Merge remote-tracking branch 'origin/sta..." | Re-trigger Greptile