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 4 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
|
| _CONFIG_DIR = Path(_env_config_dir or (Path.home() / ".claude")).expanduser().resolve() | ||
| AUDIT_LOG = _CONFIG_DIR / "hooks" / "agent-audit.log" | ||
| ERROR_LOG = _CONFIG_DIR / "hooks" / "error.log" | ||
| LAST_REPORT_FILE = _CONFIG_DIR / "hooks" / ".last_error_report" |
There was a problem hiding this comment.
Hook config dir ignores CLI arg
High Severity
setup.py honors --config-dir over CLAUDE_CONFIG_DIR, but unbound.py sets _CONFIG_DIR only from the env var or ~/.claude. After install with --config-dir alone, Claude still runs the baked hook under that directory while audit logs, policy cache, approval markers, and related paths resolve under the default config tree—breaking the end-to-end custom config path this change targets.
Additional Locations (1)
Reviewed by Cursor Bugbot for commit 3cdefe1. Configure here.
🛡️ 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 |
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes using default effort and found 1 potential issue.
There are 2 total unresolved issues (including 1 from previous review).
❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.
Reviewed by Cursor Bugbot for commit aa7f558. Configure here.
|
|
||
| if clear_mode: | ||
| clear_setup() | ||
| clear_setup(config_dir) |
There was a problem hiding this comment.
Clear misses custom config directory
High Severity
Install can target a custom Claude config tree via --config-dir, but the installer never persists that path (for example as CLAUDE_CONFIG_DIR). A later setup.py --clear or cron --backfill resolves the config directory only from the environment or default ~/.claude, so hooks and settings under the custom directory can remain while clear appears to succeed.
Additional Locations (2)
Reviewed by Cursor Bugbot for commit aa7f558. Configure here.
🛡️ 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 |


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, logs, and settings are read/written—misaligned
CLAUDE_CONFIG_DIRat install vs runtime can silently disable enforcement. The installer does not persistCLAUDE_CONFIG_DIRto the shell profile, so--config-dir-only installs may not match future Claude sessions unless the env is set elsewhere.Overview
Fixes WEB-4882 by stopping hardcoded
~/.claudepaths when Claude Code uses a custom config directory.setup.pyadds_resolve_claude_config_dir(CLAUDE_CONFIG_DIR→--config-dir→~/.claude, withexpanduser().resolve()).main()resolves once and passes thatconfig_dirthrough install,--clear, gateway artifact cleanup, install-state detection, and--backfill(transcripts underconfig_dir/projects, cutoff state underconfig_dir/hooks).unbound.pysets_CONFIG_DIRfromCLAUDE_CONFIG_DIRat import so audit/error logs, policy cache, approval markers, and self-update target the same tree. MCP config prefersconfig_dir/.claude.jsonwhen the env is set and the file exists; otherwise it keeps~/.claude.json.~/.unbound/*stays home-based.Tests cover resolution precedence, hooks/settings with baked absolute commands under a custom dir, default-dir backward compatibility, and custom-dir backfill.
Reviewed by Cursor Bugbot for commit aa7f558. Bugbot is set up for automated code reviews on this repo. Configure here.
Greptile Summary
This PR fixes silent policy-enforcement and telemetry loss by honoring
CLAUDE_CONFIG_DIR(and a new--config-dirCLI flag) throughout the installer and runtime hook, replacing all hardcoded~/.claudepaths with a resolved config root.setup.py: Adds_resolve_claude_config_dirand threads the resolvedconfig_dirthrough hook download,settings.jsonbaked commands, clear/uninstall, install-state detection, and backfill discovery.unbound.py: Resolves_CONFIG_DIRfromCLAUDE_CONFIG_DIRat import time so audit/error logs, policy cache, approval markers, and the self-update target all live under the custom config tree.test_setup.py: Updates existing backfill helpers to acceptconfig_dirand adds coverage for precedence, install paths, backward compatibility, and custom-dir backfill.Confidence Score: 4/5
Safe to merge after fixing the --config-dir / CLAUDE_CONFIG_DIR precedence order in _resolve_claude_config_dir.
The resolver checks the environment variable before the CLI flag, so any user who has CLAUDE_CONFIG_DIR in their shell and passes an explicit --config-dir to override it will have their flag silently ignored. Every other change — threading config_dir through all install paths, the unbound.py module-level resolution, backward-compatible defaults — looks correct and well-tested.
claude-code/hooks/setup.py (_resolve_claude_config_dir precedence logic) and claude-code/hooks/test_setup.py (test_env_beats_arg_and_home codifies the wrong order).
Important Files Changed
Flowchart
%%{init: {'theme': 'neutral'}}%% flowchart TD A[setup.py main] --> B[_resolve_claude_config_dir] B --> C{CLAUDE_CONFIG_DIR set?} C -- Yes --> D[use env var path] C -- No --> E{--config-dir arg present?} E -- Yes --> F[use arg path] E -- No --> G[use ~/.claude default] D --> H[config_dir: Path] F --> H G --> H H --> I[setup_hooks config_dir] H --> J[configure_claude_settings config_dir] H --> K[detect_install_state config_dir] H --> L[remove_gateway_artifacts config_dir] H --> M[run_backfill config_dir] H --> N[clear_setup config_dir] I --> O[config_dir/hooks/unbound.py] J --> P[config_dir/settings.json baked hook command] M --> Q[config_dir/projects/ config_dir/hooks/backfill-state]%%{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[setup.py main] --> B[_resolve_claude_config_dir] B --> C{CLAUDE_CONFIG_DIR set?} C -- Yes --> D[use env var path] C -- No --> E{--config-dir arg present?} E -- Yes --> F[use arg path] E -- No --> G[use ~/.claude default] D --> H[config_dir: Path] F --> H G --> H H --> I[setup_hooks config_dir] H --> J[configure_claude_settings config_dir] H --> K[detect_install_state config_dir] H --> L[remove_gateway_artifacts config_dir] H --> M[run_backfill config_dir] H --> N[clear_setup config_dir] I --> O[config_dir/hooks/unbound.py] J --> P[config_dir/settings.json baked hook command] M --> Q[config_dir/projects/ config_dir/hooks/backfill-state]Reviews (4): Last reviewed commit: "WEB-4882: resolve config dir env-first s..." | Re-trigger Greptile