Skip to content

fix: add guard cmds on managed on top of user levels#269

Merged
aarlaud merged 2 commits intomainfrom
fix/add-managed-settings-guard-capabilities
Apr 13, 2026
Merged

fix: add guard cmds on managed on top of user levels#269
aarlaud merged 2 commits intomainfrom
fix/add-managed-settings-guard-capabilities

Conversation

@aarlaud
Copy link
Copy Markdown
Contributor

@aarlaud aarlaud commented Apr 13, 2026

Add --managed flag to Agent Guard hook install/uninstall

Adds support for installing and uninstalling Agent Guard hooks at OS-specific managed (MDM/admin-deployed) config paths, in addition to the existing user-level paths.

@aarlaud aarlaud requested a review from a team as a code owner April 13, 2026 13:02
@qodo-merge-etso

This comment was marked as outdated.

@aarlaud aarlaud force-pushed the fix/add-managed-settings-guard-capabilities branch from cb1e58f to a131c9a Compare April 13, 2026 13:05
@qodo-merge-etso
Copy link
Copy Markdown

Review Summary by Qodo

Add managed (MDM/admin) config path support to Agent Guard

✨ Enhancement

Grey Divider

Walkthroughs

Description
• Add --managed flag to guard install/uninstall commands
• Define OS-specific managed config paths for Claude/Cursor
• Update status command to display both user and managed hooks
• Add comprehensive tests for managed path resolution and operations
Diagram
flowchart LR
  CLI["CLI Arguments<br/>--managed flag"]
  PATHS["OS-Specific Paths<br/>macOS/Linux/Windows"]
  CONFIG["Config Path Resolution<br/>_config_path function"]
  INSTALL["Install/Uninstall<br/>to managed paths"]
  STATUS["Status Display<br/>user + managed hooks"]
  
  CLI --> CONFIG
  PATHS --> CONFIG
  CONFIG --> INSTALL
  CONFIG --> STATUS
Loading

Grey Divider

File Changes

1. src/agent_scan/cli.py ✨ Enhancement +12/-0

Add --managed flag to install/uninstall parsers

• Add --managed argument to guard install parser
• Add --managed argument to guard uninstall parser
• Both flags use store_true action with default False

src/agent_scan/cli.py


2. src/agent_scan/guard.py ✨ Enhancement +39/-9

Implement managed paths and scope handling

• Define OS-specific managed config paths for Claude and Cursor (macOS, Windows, Linux)
• Update _config_path() function to accept managed parameter and return appropriate path
• Modify _run_install() to handle managed flag and display scope in messages
• Modify _run_uninstall() to handle managed flag and display scope in messages
• Enhance _run_status() to display both user-level and managed hooks with separate sections
• Update help text to document managed flow usage

src/agent_scan/guard.py


3. tests/unit/test_guard.py 🧪 Tests +128/-0

Add comprehensive tests for managed paths

• Import new managed path constants and _config_path function
• Add TestConfigPath class with 6 tests for path resolution logic
• Add TestManagedPathConstants class with 10 tests validating OS-specific paths
• Add TestManagedInstallClaude class with 3 tests for managed Claude operations
• Add TestManagedInstallCursor class with 3 tests for managed Cursor operations

tests/unit/test_guard.py


Grey Divider

Qodo Logo

@qodo-merge-etso
Copy link
Copy Markdown

qodo-merge-etso bot commented Apr 13, 2026

Code Review by Qodo

🐞 Bugs (0) 📘 Rule violations (0) 📎 Requirement gaps (0)

Grey Divider


Action required

1. Minted key not revoked🐞
Description
Interactive guard install mints a push key before writing/copying hook artifacts; if a later step
fails (very likely with --managed due to privileged directories), the minted push key is left
active because revocation only happens when the test event fails.
Code

src/agent_scan/guard.py[R116-130]

    push_key = os.environ.get("PUSH_KEY", "")
    headless = bool(push_key)
    tenant_id: str = getattr(args, "tenant_id", None) or ""
+    managed: bool = getattr(args, "managed", False)

    label = _client_label(client)
+    scope = "managed" if managed else "user"
    snyk_token = ""

    if not headless:
        # Interactive flow — mint a push key
-        rich.print(f"Installing [bold magenta]Agent Guard[/bold magenta] hooks for [bold]{label}[/bold]")
+        rich.print(f"Installing [bold magenta]Agent Guard[/bold magenta] {scope} hooks for [bold]{label}[/bold]")
        rich.print()

        snyk_token = os.environ.get("SNYK_TOKEN", "")
Evidence
After minting the push key, the code proceeds to resolve the (possibly managed) config path and
create/copy the hook script. _copy_hook_script() creates directories (mkdir) next to the config
path, which can raise PermissionError for managed locations; run_guard() catches that
PermissionError and exits without any cleanup. The only automatic revocation path today is inside
the "test event failed" branch, not for general install failures.

src/agent_scan/guard.py[113-165]
src/agent_scan/guard.py[169-182]
src/agent_scan/guard.py[661-685]
src/agent_scan/guard.py[82-97]

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

### Issue description
When `guard install` runs interactively, it mints a push key and then performs filesystem operations that can fail (especially with `--managed`). If any exception occurs after minting (e.g., `PermissionError` while creating/copying hooks), the minted push key is not revoked.

### Issue Context
This is more likely now because managed paths are typically privileged (e.g., `/etc`, `/Library/Application Support`, `C:/Program Files`). Leaving minted keys active after a failed install is a credential hygiene/security issue.

### Fix Focus Areas
- src/agent_scan/guard.py[113-201]
- src/agent_scan/guard.py[661-685]
- src/agent_scan/guard.py[82-97]

### Implementation notes
- Track whether the push key was minted in this run (already done via `minted`).
- Wrap all post-mint operations (config path resolution, script copy, test event, config edits) in a `try` block and add a `finally` (or `except`+rethrow) that attempts `revoke_push_key(...)` when `minted` is True and install did not complete successfully.
- Consider a quick preflight before minting when `--managed` is set (e.g., check writability/ability to create `config_path.parent`) to avoid minting keys that cannot be installed.

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


2. Status breaks on unreadable managed🐞
Description
_run_status() always probes managed config paths; if an MDM/admin-deployed config exists but is
unreadable, _read_json_or_empty() raises PermissionError and run_guard() exits 1, so
user-level hook status cannot be displayed.
Code

src/agent_scan/guard.py[R364-376]

+    rich.print("[bold]User-level hooks:[/bold]")
    _print_client_status("Claude Code", CLAUDE_SETTINGS_PATH, _detect_claude_install())
    rich.print()
    _print_client_status("Cursor", CURSOR_HOOKS_PATH, _detect_cursor_install())
    rich.print()
-    rich.print("[dim]# interactive flow[/dim]")
+
+    rich.print("[bold]Managed hooks:[/bold]")
+    _print_client_status(
+        "Claude Code", CLAUDE_MANAGED_SETTINGS_PATH, _detect_claude_install(CLAUDE_MANAGED_SETTINGS_PATH)
+    )
+    rich.print()
+    _print_client_status("Cursor", CURSOR_MANAGED_HOOKS_PATH, _detect_cursor_install(CURSOR_MANAGED_HOOKS_PATH))
+    rich.print()
Evidence
_run_status() unconditionally calls detection on managed paths, and detection reads JSON via
_read_json_or_empty() which uses open() without handling PermissionError. Since run_guard()
catches PermissionError at the top level and returns 1, a permission problem on the managed file
aborts the entire status command (including user-level output).

src/agent_scan/guard.py[82-97]
src/agent_scan/guard.py[363-376]
src/agent_scan/guard.py[408-437]
src/agent_scan/guard.py[704-709]

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

### Issue description
`guard` status should not fail entirely when managed (admin/MDM) config files exist but are unreadable. Today `_run_status()` always reads managed paths and a `PermissionError` aborts the whole command via `run_guard()`.

### Issue Context
Managed configs are commonly deployed with root/admin ownership and restrictive permissions. `guard` status should still report user-level status even if managed status cannot be read.

### Fix Focus Areas
- src/agent_scan/guard.py[363-376]
- src/agent_scan/guard.py[408-452]
- src/agent_scan/guard.py[704-709]
- src/agent_scan/guard.py[82-97]

### Implementation notes
- Wrap managed-path detection calls in `_run_status()` with `try/except PermissionError` and display a clear “PERMISSION DENIED”/“UNREADABLE” status for that section while continuing.
- Alternatively, make `_detect_claude_install` / `_detect_cursor_install` (or `_read_json_or_empty`) catch `PermissionError` and return `None` (or a sentinel) for status display, without impacting install/uninstall behavior.

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



Remediation recommended

3. _run_install() exceeds 40 lines📘
Description
The modified _run_install() function is ~89 lines long, exceeding the ≤40 SLOC limit and making
the install flow harder to maintain and safely extend. The new managed/user branching adds more
logic into an already monolithic function instead of being extracted into helpers.
Code

src/agent_scan/guard.py[R116-123]

    push_key = os.environ.get("PUSH_KEY", "")
    headless = bool(push_key)
    tenant_id: str = getattr(args, "tenant_id", None) or ""
+    managed: bool = getattr(args, "managed", False)

    label = _client_label(client)
+    scope = "managed" if managed else "user"
    snyk_token = ""
Evidence
PR Compliance ID 45 requires functions to be ≤40 lines; _run_install() (which this PR changes)
spans far more than 40 lines, and the added managed/scope logic further increases its size
within the same function.

Rule 45: Limit function length to ≤ 40 lines (SLOC)
src/agent_scan/guard.py[113-200]

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

## Issue description
`_run_install()` is significantly longer than the 40-line limit and the PR adds additional branching, increasing maintenance risk.

## Issue Context
The managed/user install flow, push-key minting, test-event verification, and config editing are all handled in a single function.

## Fix Focus Areas
- src/agent_scan/guard.py[113-200]

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


Grey Divider

ⓘ The new review experience is currently in Beta. Learn more

Grey Divider

Qodo Logo

@aarlaud aarlaud force-pushed the fix/add-managed-settings-guard-capabilities branch 2 times, most recently from 3345739 to a131c9a Compare April 13, 2026 13:27
@aarlaud aarlaud force-pushed the fix/add-managed-settings-guard-capabilities branch from 2307774 to a692f7c Compare April 13, 2026 17:11
@aarlaud aarlaud merged commit b75f9dc into main Apr 13, 2026
8 checks passed
@aarlaud aarlaud deleted the fix/add-managed-settings-guard-capabilities branch April 13, 2026 17:31
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