Skip to content

fix(guard): honor client config home overrides - #444

Open
lbeurerkellner wants to merge 3 commits into
mainfrom
codex/fix-guard-client-config-home
Open

fix(guard): honor client config home overrides#444
lbeurerkellner wants to merge 3 commits into
mainfrom
codex/fix-guard-client-config-home

Conversation

@lbeurerkellner

@lbeurerkellner lbeurerkellner commented Aug 21, 2026

Copy link
Copy Markdown
Contributor

Summary

Resolve Claude and Codex user configuration directories at runtime, honoring CLAUDE_CONFIG_DIR and CODEX_HOME for discovery and guard setup when they are the only installed location.

Enforcement safety

When both an overridden directory and the standard client directory exist, guard installation now prefers the standard location so an environment override cannot redirect hooksConfigured enforcement into an unused directory. Added end-to-end guard tests for both clients and both path-selection cases.

Validation

The full guard unit suite passes (242 passed, 11 skipped), along with mypy, Ruff, formatting, and repository pre-commit hooks.

@lbeurerkellner
lbeurerkellner requested a review from a team as a code owner August 21, 2026 07:20
@qodo-merge-etso

Copy link
Copy Markdown

PR Summary by Qodo

Fix guard/discovery to resolve client config dirs at runtime (CLAUDE_CONFIG_DIR/CODEX_HOME)

🐞 Bug fix 🧪 Tests 🕐 40+ Minutes

Grey Divider

AI Description

• Centralize runtime resolution of Claude/Codex/Cursor user config directories (incl. env
 overrides).
• Update guard install/status/detection to honor CLAUDE_CONFIG_DIR and CODEX_HOME.
• Add unit coverage for tilde expansion, runtime env changes, and resolved-path guard behavior.
Diagram

graph TD
  G["guard.py (install/status)"] --> R["resolve_user_client_dir()"] --> P[("Client config dir")] --> F[("settings.json / hooks.json")]
  D["claude_code.py / codex.py"] --> R
  E[("Env overrides\nCLAUDE_CONFIG_DIR/CODEX_HOME")] --> O["user_client_dir_override()"] --> R
  H[("Home defaults\n~/.claude ~/.codex ~/.cursor")] --> R
Loading
High-Level Assessment

The following are alternative approaches to this PR:

1. Adopt platformdirs/appdirs-style resolution
  • ➕ Standardized per-OS config locations and conventions
  • ➕ Less bespoke mapping logic over time
  • ➖ Adds/changes dependency surface area
  • ➖ May not match actual client-specific locations (Claude/Codex conventions are explicit)
  • ➖ Still need env override handling and multi-user scan semantics
2. Keep per-client resolution logic in each module
  • ➕ No new shared module
  • ➕ Changes stay localized to each discoverer/guard section
  • ➖ Duplicated logic across guard + discoverers
  • ➖ Higher chance of drift/inconsistencies (e.g., tilde expansion, runtime env changes)

Recommendation: The chosen approach (a small shared resolver used by both discoverers and guard) is the best fit: it fixes the import-time-constant bug, keeps multi-user scanning semantics explicit via honor_environment, and avoids introducing a heavier dependency that may not align with client-specific path conventions.

Files changed (6) +310 / -60

Bug fix (3) +37 / -34
claude_code.pyResolve Claude base dir via shared runtime client-path resolver +7/-6

Resolve Claude base dir via shared runtime client-path resolver

• Replaces direct 'CLAUDE_CONFIG_DIR' checks with 'resolve_user_client_dir()' to compute the Claude config base directory at runtime. Updates the relocated '.claude.json' probing logic to rely on the shared override detector (and thus tilde expansion) when scanning the current user.

src/agent_scan/agents/claude_code.py

codex.pyResolve Codex home via shared runtime client-path resolver +6/-5

Resolve Codex home via shared runtime client-path resolver

• Replaces direct 'CODEX_HOME' handling with 'resolve_user_client_dir()' so Codex config discovery honors overrides only for own-home scans. Aligns Codex behavior with Claude’s multi-user scan constraints.

src/agent_scan/agents/codex.py

guard.pyUse runtime-resolved client config paths for guard status/install detection +24/-23

Use runtime-resolved client config paths for guard status/install detection

• Removes user-level path constants and replaces them with runtime '_config_path()' composition based on 'resolve_user_client_dir()' and per-client config filenames. Updates status output and install detectors to accept optional paths (defaulting to resolved paths), and updates installed-client checks to use the resolver so env changes after import are honored.

src/agent_scan/guard.py

Refactor (1) +52 / -0
client_paths.pyAdd shared resolver for per-client user config directories +52/-0

Add shared resolver for per-client user config directories

• Introduces a central mapping of clients to (env var, default dir) and two helpers: 'user_client_dir_override()' (env read + '~' expansion) and 'resolve_user_client_dir()' (runtime resolution with an explicit 'honor_environment' switch). This prevents import-time path capture and standardizes override semantics across the codebase.

src/agent_scan/client_paths.py

Tests (2) +221 / -26
test_agent_discovery.pyAdd discovery tests for tilde expansion in CLAUDE_CONFIG_DIR/CODEX_HOME +28/-0

Add discovery tests for tilde expansion in CLAUDE_CONFIG_DIR/CODEX_HOME

• Adds unit tests asserting that '~' in 'CLAUDE_CONFIG_DIR' and 'CODEX_HOME' expands relative to 'HOME' and that the relocated config files are discovered correctly. Complements existing tests that ensure overrides are ignored when scanning other users’ homes.

tests/unit/test_agent_discovery.py

test_guard.pyExpand guard tests for resolved paths, env override behavior, and status/install flows +193/-26

Expand guard tests for resolved paths, env override behavior, and status/install flows

• Updates config-path tests to cover env override precedence, empty env handling, tilde expansion, and runtime env changes post-import. Adds end-to-end style unit tests verifying install/uninstall/status and detector logic operate against resolved paths, and refactors mocks to patch 'resolve_user_client_dir' rather than removed constants.

tests/unit/test_guard.py

@qodo-merge-etso

qodo-merge-etso Bot commented Aug 21, 2026

Copy link
Copy Markdown

Code Review by Qodo

🐞 Bugs (2) 📘 Rule violations (0) 📜 Skill insights (0)

Grey Divider


Action required

1. Relative override path writes 🐞 Bug ⛨ Security
Description
If CLAUDE_CONFIG_DIR/CODEX_HOME is set to a relative path, resolve_user_client_dir() returns
it as-is and guard builds config/script paths relative to the current working directory, causing
hook install/uninstall/status to read/write the wrong location. This can create or delete files in
unexpected directories and makes behavior dependent on the process CWD.
Code

src/agent_scan/client_paths.py[R23-26]

+    value = os.environ.get(env_var)
+    if not value:
+        return None
+    return Path(value).expanduser()
Relevance

●●● Strong

Relative overrides directly enable CWD-dependent writes; recent security/path-safety precedents
favor fixing concrete path hazards.

PR-#233
PR-#348

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
The override resolver returns an expanded path without enforcing it is absolute, and guard directly
uses that directory to compute config paths and create/write the hooks directory and script file, so
a relative override becomes CWD-relative writes.

src/agent_scan/client_paths.py[15-27]
src/agent_scan/client_paths.py[46-52]
src/agent_scan/guard.py[1195-1206]
src/agent_scan/guard.py[1268-1297]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

### Issue description
`user_client_dir_override()` returns `Path(value).expanduser()` without ensuring the result is absolute. When the env var is set to a relative path (e.g. `CODEX_HOME=custom-codex`), downstream code (notably `guard._config_path()` and `_copy_hook_script()`) will create directories and write files relative to the current working directory.

### Issue Context
This PR newly makes guard honor `CLAUDE_CONFIG_DIR` and `CODEX_HOME` via `resolve_user_client_dir()`, so the relative-path edge case now affects guard install/uninstall/status behavior.

### Fix Focus Areas
- src/agent_scan/client_paths.py[15-52]
- src/agent_scan/guard.py[1195-1206]

### Suggested fix
- In `user_client_dir_override()` (or in `resolve_user_client_dir()` right after reading `configured`), normalize the path:
 - `p = Path(value).expanduser()`
 - If `not p.is_absolute()`: either
   - raise a `ValueError` with a clear message requiring an absolute path, **or**
   - treat it as relative to the scanning user’s home (e.g. `p = (Path.home() / p)`), then return `p`.
- Add a unit test for guard ensuring a relative `CODEX_HOME`/`CLAUDE_CONFIG_DIR` is rejected or anchored deterministically (depending on chosen behavior).

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools



Informational

2. Client path metadata duplication 🐞 Bug ⚙ Maintainability
Description
Client directory/default metadata now exists in multiple places (_CLIENT_DIR_SETTINGS and guard’s
_CLIENT_CONFIG_FILENAMES), which can drift and cause runtime ValueError/KeyError if a client
is added/renamed in only one mapping. This increases maintenance risk for future client additions or
path changes.
Code

src/agent_scan/guard.py[R44-48]

+_CLIENT_CONFIG_FILENAMES = {
+    "claude": "settings.json",
+    "cursor": "hooks.json",
+    "codex": "hooks.json",
+}
Relevance

●● Moderate

Centralization is often accepted, but this is a subjective refactor without decisive same-context
precedent; mappings have distinct responsibilities.

PR-#337
PR-#321

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
The PR adds a new centralized mapping for client directories while also adding a separate mapping in
guard for config filenames; these two sources must stay aligned to avoid resolution/indexing errors.

src/agent_scan/client_paths.py[8-12]
src/agent_scan/guard.py[44-48]
src/agent_scan/guard.py[1195-1206]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

### Issue description
Client path configuration is split across multiple dicts:
- `client_paths._CLIENT_DIR_SETTINGS` (env var + default dir)
- `guard._CLIENT_CONFIG_FILENAMES` (config filename)
This duplication can drift, causing failures when one mapping is updated without the other.

### Issue Context
This PR introduced `client_paths.py` specifically to centralize runtime directory resolution, but guard still carries a separate client mapping for filenames.

### Fix Focus Areas
- src/agent_scan/client_paths.py[8-52]
- src/agent_scan/guard.py[44-48]

### Suggested fix
- Create a single source of truth (e.g. a `ClientPathSpec` dataclass) in `client_paths.py` that includes env var, default dir, and (optionally) config filename.
- Import and use that spec in guard (and optionally in discoverers) to avoid parallel mappings.
- Add a small assertion test ensuring all clients supported by guard are present in the shared spec (and vice versa, if appropriate).

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


Grey Divider

Context sources
✅ Compliance rules (platform): 8 rules

Grey Divider

Tip of the day
💡 Did you know, you can tweak Display preferences with a live preview to see your comment before it ships

More tips ↗ | Customize Qodo ↗ | Qodo docs ↗

Grey Divider

Qodo Logo

Comment on lines +23 to +26
value = os.environ.get(env_var)
if not value:
return None
return Path(value).expanduser()

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Action required

1. Relative override path writes 🐞 Bug ⛨ Security

If CLAUDE_CONFIG_DIR/CODEX_HOME is set to a relative path, resolve_user_client_dir() returns
it as-is and guard builds config/script paths relative to the current working directory, causing
hook install/uninstall/status to read/write the wrong location. This can create or delete files in
unexpected directories and makes behavior dependent on the process CWD.
Agent Prompt
### Issue description
`user_client_dir_override()` returns `Path(value).expanduser()` without ensuring the result is absolute. When the env var is set to a relative path (e.g. `CODEX_HOME=custom-codex`), downstream code (notably `guard._config_path()` and `_copy_hook_script()`) will create directories and write files relative to the current working directory.

### Issue Context
This PR newly makes guard honor `CLAUDE_CONFIG_DIR` and `CODEX_HOME` via `resolve_user_client_dir()`, so the relative-path edge case now affects guard install/uninstall/status behavior.

### Fix Focus Areas
- src/agent_scan/client_paths.py[15-52]
- src/agent_scan/guard.py[1195-1206]

### Suggested fix
- In `user_client_dir_override()` (or in `resolve_user_client_dir()` right after reading `configured`), normalize the path:
  - `p = Path(value).expanduser()`
  - If `not p.is_absolute()`: either
    - raise a `ValueError` with a clear message requiring an absolute path, **or**
    - treat it as relative to the scanning user’s home (e.g. `p = (Path.home() / p)`), then return `p`.
- Add a unit test for guard ensuring a relative `CODEX_HOME`/`CLAUDE_CONFIG_DIR` is rejected or anchored deterministically (depending on chosen behavior).

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

Comment thread src/agent_scan/guard.py
Comment on lines +44 to +48
_CLIENT_CONFIG_FILENAMES = {
"claude": "settings.json",
"cursor": "hooks.json",
"codex": "hooks.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.

Informational

2. Client path metadata duplication 🐞 Bug ⚙ Maintainability

Client directory/default metadata now exists in multiple places (_CLIENT_DIR_SETTINGS and guard’s
_CLIENT_CONFIG_FILENAMES), which can drift and cause runtime ValueError/KeyError if a client
is added/renamed in only one mapping. This increases maintenance risk for future client additions or
path changes.
Agent Prompt
### Issue description
Client path configuration is split across multiple dicts:
- `client_paths._CLIENT_DIR_SETTINGS` (env var + default dir)
- `guard._CLIENT_CONFIG_FILENAMES` (config filename)
This duplication can drift, causing failures when one mapping is updated without the other.

### Issue Context
This PR introduced `client_paths.py` specifically to centralize runtime directory resolution, but guard still carries a separate client mapping for filenames.

### Fix Focus Areas
- src/agent_scan/client_paths.py[8-52]
- src/agent_scan/guard.py[44-48]

### Suggested fix
- Create a single source of truth (e.g. a `ClientPathSpec` dataclass) in `client_paths.py` that includes env var, default dir, and (optionally) config filename.
- Import and use that spec in guard (and optionally in discoverers) to avoid parallel mappings.
- Add a small assertion test ensuring all clients supported by guard are present in the shared spec (and vice versa, if appropriate).

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants