-
Notifications
You must be signed in to change notification settings - Fork 265
fix(guard): honor client config home overrides #444
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
lbeurerkellner
wants to merge
3
commits into
main
Choose a base branch
from
codex/fix-guard-client-config-home
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from all commits
Commits
Show all changes
3 commits
Select commit
Hold shift + click to select a range
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,56 @@ | ||
| """Runtime resolution of user-level client configuration directories.""" | ||
|
|
||
| from __future__ import annotations | ||
|
|
||
| import os | ||
| from pathlib import Path | ||
|
|
||
| _CLIENT_DIR_SETTINGS: dict[str, tuple[str | None, str]] = { | ||
| "claude": ("CLAUDE_CONFIG_DIR", ".claude"), | ||
| "cursor": (None, ".cursor"), | ||
| "codex": ("CODEX_HOME", ".codex"), | ||
| } | ||
|
|
||
|
|
||
| def user_client_dir_override(client: str) -> Path | None: | ||
| """Return a non-empty environment path with its user marker expanded.""" | ||
| try: | ||
| env_var, _ = _CLIENT_DIR_SETTINGS[client] | ||
| except KeyError as exc: | ||
| raise ValueError(f"Unknown client: {client}") from exc | ||
| if env_var is None: | ||
| return None | ||
| value = os.environ.get(env_var) | ||
| if not value: | ||
| return None | ||
| if value == "~": | ||
| return Path(os.environ.get("HOME") or Path.home()) | ||
| if value.startswith(("~/", "~\\")) and os.environ.get("HOME"): | ||
| return Path(os.environ["HOME"]) / value[2:] | ||
| return Path(value).expanduser() | ||
|
|
||
|
|
||
| def resolve_user_client_dir( | ||
| client: str, | ||
| *, | ||
| home_directory: Path | None = None, | ||
| honor_environment: bool = True, | ||
| ) -> Path: | ||
| """Resolve a client's user-level configuration directory at call time. | ||
|
|
||
| ``home_directory`` supports discovery of a specific user's default directory. | ||
| Environment overrides describe only the current process's user, so callers | ||
| scanning another user's home must pass ``honor_environment=False``. | ||
| """ | ||
| try: | ||
| env_var, default_dir = _CLIENT_DIR_SETTINGS[client] | ||
| except KeyError as exc: | ||
| raise ValueError(f"Unknown client: {client}") from exc | ||
|
|
||
| if honor_environment and env_var: | ||
| configured = user_client_dir_override(client) | ||
| if configured is not None: | ||
| return configured | ||
|
|
||
| home = Path.home() if home_directory is None else home_directory | ||
| return home / default_dir | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -17,6 +17,7 @@ | |
|
|
||
| import rich | ||
|
|
||
| from agent_scan.client_paths import resolve_user_client_dir | ||
| from agent_scan.pushkeys import ( | ||
| GuardEnabledAccessDeniedError, | ||
| _is_localhost, | ||
|
|
@@ -40,9 +41,13 @@ | |
| ) | ||
| _PERMISSION_DENIED = "__permission_denied__" | ||
|
|
||
| CLAUDE_SETTINGS_PATH = Path.home() / ".claude" / "settings.json" | ||
| CURSOR_HOOKS_PATH = Path.home() / ".cursor" / "hooks.json" | ||
| CODEX_HOOKS_PATH = Path.home() / ".codex" / "hooks.json" | ||
| _CLIENT_CONFIG_FILENAMES = { | ||
| "claude": "settings.json", | ||
| "cursor": "hooks.json", | ||
| "codex": "hooks.json", | ||
| } | ||
|
Comment on lines
+44
to
+48
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 2. Client path metadata duplication 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
|
||
| _CLIENT_DEFAULT_DIRNAMES = {"claude": ".claude", "cursor": ".cursor", "codex": ".codex"} | ||
| _CLIENT_HOME_ENV_VARS = {"claude": "CLAUDE_CONFIG_DIR", "codex": "CODEX_HOME"} | ||
|
|
||
| # Managed (MDM / admin-deployed) config paths — OS-specific | ||
| # Codex managed hooks use a requirements.toml file at a system location | ||
|
|
@@ -792,11 +797,14 @@ def _uninstall_cursor(path: Path) -> None: | |
|
|
||
| def _run_status() -> None: | ||
| rich.print("[bold]User-level hooks:[/bold]") | ||
| _print_client_status("Claude Code", CLAUDE_SETTINGS_PATH, _detect_claude_install()) | ||
| claude_path = _config_path("claude") | ||
| _print_client_status("Claude Code", claude_path, _detect_claude_install(claude_path)) | ||
| rich.print() | ||
| _print_client_status("Cursor", CURSOR_HOOKS_PATH, _detect_cursor_install()) | ||
| cursor_path = _config_path("cursor") | ||
| _print_client_status("Cursor", cursor_path, _detect_cursor_install(cursor_path)) | ||
| rich.print() | ||
| _print_client_status("Codex", CODEX_HOOKS_PATH, _detect_codex_install()) | ||
| codex_path = _config_path("codex") | ||
| _print_client_status("Codex", codex_path, _detect_codex_install(codex_path)) | ||
| rich.print() | ||
|
|
||
| rich.print("[bold]Managed hooks:[/bold]") | ||
|
|
@@ -855,7 +863,9 @@ def _print_client_status(label: str, path: Path, info: dict | str | None) -> Non | |
| ) | ||
|
|
||
|
|
||
| def _detect_claude_install(path: Path = CLAUDE_SETTINGS_PATH) -> dict | None: | ||
| def _detect_claude_install(path: Path | None = None) -> dict | None: | ||
| if path is None: | ||
| path = _config_path("claude") | ||
| if not path.exists(): | ||
| return None | ||
| settings = _read_json_or_empty(path) | ||
|
|
@@ -880,7 +890,9 @@ def _detect_claude_install(path: Path = CLAUDE_SETTINGS_PATH) -> dict | None: | |
| return _parse_command_info(found_cmd, events) | ||
|
|
||
|
|
||
| def _detect_codex_install(path: Path = CODEX_HOOKS_PATH) -> dict | None: | ||
| def _detect_codex_install(path: Path | None = None) -> dict | None: | ||
| if path is None: | ||
| path = _config_path("codex") | ||
| if not path.exists(): | ||
| return None | ||
| if _is_codex_requirements_toml(path): | ||
|
|
@@ -907,7 +919,9 @@ def _detect_codex_install(path: Path = CODEX_HOOKS_PATH) -> dict | None: | |
| return _parse_command_info(found_cmd, events) | ||
|
|
||
|
|
||
| def _detect_cursor_install(path: Path = CURSOR_HOOKS_PATH) -> dict | None: | ||
| def _detect_cursor_install(path: Path | None = None) -> dict | None: | ||
| if path is None: | ||
| path = _config_path("cursor") | ||
| if not path.exists(): | ||
| return None | ||
| data = _read_json_or_empty(path) | ||
|
|
@@ -1160,18 +1174,11 @@ def _extract_env_from_cmd(cmd: str, key: str) -> str: | |
| _HOOK_CLIENT_NAMES = {"claude": "claude-code", "cursor": "cursor", "codex": "codex"} | ||
|
|
||
|
|
||
| _CLIENT_INSTALL_PATHS = { | ||
| "claude": Path.home() / ".claude", | ||
| "cursor": Path.home() / ".cursor", | ||
| "codex": Path.home() / ".codex", | ||
| } | ||
|
|
||
|
|
||
| def _is_client_installed(client: str) -> bool: | ||
| """Check whether the agent is installed on this machine by looking for its config directory.""" | ||
| path = _CLIENT_INSTALL_PATHS.get(client) | ||
| if path is None: | ||
| if client not in _CLIENT_CONFIG_FILENAMES: | ||
| return True | ||
| path = _guard_user_client_dir(client) | ||
| try: | ||
| return path.is_dir() | ||
| except PermissionError: | ||
|
|
@@ -1197,11 +1204,18 @@ def _config_path(client: str, override: str | None = None, managed: bool = False | |
| if client == "cursor": | ||
| return CURSOR_MANAGED_HOOKS_PATH | ||
| return CODEX_MANAGED_HOOKS_PATH | ||
| if client == "claude": | ||
| return CLAUDE_SETTINGS_PATH | ||
| if client == "cursor": | ||
| return CURSOR_HOOKS_PATH | ||
| return CODEX_HOOKS_PATH | ||
| return _guard_user_client_dir(client) / _CLIENT_CONFIG_FILENAMES[client] | ||
|
|
||
|
|
||
| def _guard_user_client_dir(client: str) -> Path: | ||
| """Resolve the user config directory without allowing an env override to hide an installed default.""" | ||
| configured = resolve_user_client_dir(client) | ||
| env_var = _CLIENT_HOME_ENV_VARS.get(client) | ||
| if env_var and os.environ.get(env_var): | ||
| default = Path.home() / _CLIENT_DEFAULT_DIRNAMES[client] | ||
| if configured != default and default.is_dir(): | ||
| return default | ||
| return configured | ||
|
|
||
|
|
||
| def _preflight_writable(config_path: Path) -> None: | ||
|
|
||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
1. Relative override path writes
🐞 Bug⛨ SecurityAgent Prompt
ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools