Skip to content

feat: send discovered servers during guard install - #443

Merged
iamcristi merged 60 commits into
mainfrom
feat/guard-serversdiscovered-event
Aug 28, 2026
Merged

feat: send discovered servers during guard install#443
iamcristi merged 60 commits into
mainfrom
feat/guard-serversdiscovered-event

Conversation

@iamcristi

@iamcristi iamcristi commented Aug 20, 2026

Copy link
Copy Markdown
Contributor

Summary

Agent Guard now reports the MCP servers it can see at install time and, when
session-start discovery is enabled, at every session start. Hook events identify the
reporting machine by an explicit machine ID rather than by hostname.

Breaking changes

  • guard install now requires --machine-id (or MACHINE_ID) and exits 1 without
    it. Existing invocations must be updated. --control-identifier is not accepted by
    guard install; it remains available only for the legacy control-server blocks used
    by scan, inspect, and evo commands.
  • X-User.identifier on hook events is now the machine ID, not the hostname. The
    POSIX forwarder requires MACHINE_ID; the PowerShell forwarder requires either
    -MachineId or MACHINE_ID. A forwarder from this branch will therefore not run
    against config written by an earlier version unless a machine ID is supplied
    externally.

New behavior

  • New internal subcommand guard discover --client {claude-code,cursor,codex} [--scope {servers,skills,all}], with snyk-agent-guard-discover.sh / .ps1
    trampolines and a new hook_events.py that POSTs hook events directly from Python
    (its own User-Agent, backend_client_session, transport-only retries).
  • guard install sends one best-effort hooksConfiguredServerDiscovery event through
    the first installed client's endpoint.
  • A SessionStart hook sends a best-effort sessionStartServerDiscovery event per
    session (async: true for Claude and Codex; Cursor's sessionStart is
    fire-and-forget by design). It is installed only when AGENT_SCAN_COMMAND is set
    at install time
    ; otherwise install prints a warning and writes no discovery hook.
  • DiscoveryScope (servers / skills / all) gates both the AgentDiscoverer
    implementations and the well-known-client path.
  • Discoverers accept target_folders from hook payloads;
    _project_paths_with_ancestors becomes _discovery_paths_with_ancestors and merges
    recorded project history with the folders named in the request.

Payload / wire changes

  • hooksConfigured.hooks_script gains discover_current_checksum and
    discover_new_checksum.
  • hooksConfiguredServerDiscovery and sessionStartServerDiscovery payloads carry
    discovery_duration_ms.
  • snyk-agent-guard.sh sends the body via --data-binary @- on stdin instead of on the
    argv.

Fixes

  • get_relative_path home matching is now boundary-aware, so /home/alicex no longer
    collapses to ~x; matching is case-insensitive on Windows.
  • scan without --skills skips the skills glob entirely instead of globbing and
    discarding the result.
  • Managed Codex hook commands are escaped as valid TOML basic strings, including
    control characters.

Refactors (no behavior change)

  • _write_claude/cursor/codex_config share _write_config;
    _uninstall_claude/cursor/codex share _uninstall_hooks; the three client-specific
    detection wrappers share _detect_install; _run_status is table-driven.
  • POSIX, PowerShell, and argv/environment hook rendering now share a single
    _HookInvocation spec.
  • _analysis_client_session is now the public backend_client_session;
    _RETRYABLE_TRANSPORT_EXCEPTIONS is public.
  • Uninstall's "No X found" message now uses the real filename (for example,
    managed-settings.json).

Verification

  • Focused guard/config/discovery unit suite: 1111 passed, 28 skipped
  • Guard uv E2E: 3 passed, 3 deselected
  • Current GitHub Actions Linux and Linux ARM test jobs: passed
  • Current pre-commit job: passed
  • Terminal stdin smoke test: returned without hanging

Known issues / follow-ups

Open on this branch:

  • _compute_hooks_diff normalizes push keys only, so an upgrade that adds MACHINE_ID=
    to the command reports every event as modified -- i.e. as customer hook tampering.
  • _servers_discovered_entries never serializes skills, so --scope skills posts no
    discovered skill data and the CLI default all pays for a skills sweep whose result
    is discarded.
  • --scope has three defaults: all in the CLI, servers in the PowerShell trampoline,
    and servers hard-coded in the installed hook.
  • _build_discover_hook_command accepts tenant_id and drops it.

Pre-existing, surfaced by this branch's refactor:

  • _render_powershell_command never emits TENANT_ID, so _parse_command_info cannot
    recover the tenant on Windows and guard uninstall never revokes the push key there.
    The new _HookInvocation already carries the field.

Cross-repo:

  • agent-monitor PR #318 adopts the
    hooksConfiguredServerDiscovery and sessionStartServerDiscovery event names.
  • agent-monitor still needs support for surfaced discovery-entry errors and enforcement
    of the new discovery-script checksums.
  • Rollout order remains agent-scan first, then strict per-session matching in
    agent-monitor.
  • Reviewers should confirm the discovery event joining the agent's real session context
    has no session-state effects beyond expectation pairing.
  • The ADS follow-up ticket still needs filing.

@iamcristi
iamcristi requested a review from a team as a code owner August 20, 2026 09:50
@qodo-merge-etso

qodo-merge-etso Bot commented Aug 20, 2026

Copy link
Copy Markdown

PR Summary by Qodo

Report discovered MCP servers during Guard install and session start

✨ Enhancement 🐞 Bug fix 🧪 Tests 📝 Documentation 🕐 40+ Minutes

Grey Divider

AI Description

• Reports discovered MCP servers after Guard installation and at enabled session starts.
• Identifies hook events by required machine IDs and includes discovery timing.
• Scopes discovery, honors hook target folders, and hardens cross-platform hook delivery.
Diagram

graph TD
  Install["guard install"] --> Config["Client hooks"] --> Session["Session start"] --> Discover["guard discover"] --> Pipeline["Discovery pipeline"] --> Event["Discovery event"] --> Monitor["Agent Monitor"]
  Install --> Pipeline
Loading
High-Level Assessment

The approach is appropriate: it reuses the existing discovery and scan-request serialization paths, isolates direct hook transport behind a shared backend session, and keeps session telemetry best-effort. Routing discovery through the existing forwarding script or introducing a persistent background service would add subprocess coupling or operational complexity without improving this install/session-start workflow.

Files changed (27) +5901 / -729

Enhancement (14) +1163 / -388
__init__.pyExpose scoped discovery and propagate target folders +8/-7

Expose scoped discovery and propagate target folders

• Exports 'DiscoveryScope' and passes request target folders into every registered discoverer.

src/agent_scan/agents/init.py

base.pyAdd scoped discovery and merged target roots +75/-23

Add scoped discovery and merged target roots

• Adds server, skill, and combined discovery scopes. Merges explicit request folders with recorded project roots and ancestors while preserving stable, resilient deduplication.

src/agent_scan/agents/base.py

claude_code.pyDiscover Claude resources from request targets +3/-3

Discover Claude resources from request targets

• Uses merged discovery paths for project MCP server and skill searches, allowing session-provided folders to supplement Claude project history.

src/agent_scan/agents/claude_code.py

codex.pyDiscover Codex resources from request targets +2/-2

Discover Codex resources from request targets

• Uses merged discovery paths for project Codex configuration and skills searches.

src/agent_scan/agents/codex.py

opencode.pyInclude explicit roots in OpenCode discovery +5/-5

Include explicit roots in OpenCode discovery

• Routes project configuration, MCP server, and skill discovery through merged request and history roots. Relative configured skill paths can now anchor to explicit targets.

src/agent_scan/agents/opencode.py

base.pyUse merged roots across VS Code discovery +6/-6

Use merged roots across VS Code discovery

• Applies explicit target folders to workspace MCP, agent configuration, skills, settings, and devcontainer discovery across the VS Code family.

src/agent_scan/agents/vscode/base.py

cli.pyAdd Guard discovery CLI and machine ID option +38/-0

Add Guard discovery CLI and machine ID option

• Adds required machine identity support to Guard installation and introduces 'guard discover' with client and scope selection. Normal scans now skip skill discovery unless requested.

src/agent_scan/cli.py

guard.pyOrchestrate install-time and session-start server reporting +784/-316

Orchestrate install-time and session-start server reporting

• Requires machine IDs, installs client-specific discovery hooks, discovers and serializes MCP servers, and sends timed install/session events. Consolidates hook rendering, configuration writing, detection, uninstall, status, script checksums, and managed Codex TOML handling.

src/agent_scan/guard.py

hook_events.pySend hook events directly to Agent Monitor +97/-0

Send hook events directly to Agent Monitor

• Adds client endpoint metadata and direct base64 hook-event delivery through the shared backend session. Uses machine IDs in 'X-User', transport-only retries, and best-effort error handling.

src/agent_scan/hook_events.py

snyk-agent-guard-discover.ps1Add Windows session discovery trampoline +54/-0

Add Windows session discovery trampoline

• Invokes 'guard discover' with inherited hook payload input, machine identity, client, and scope. Discovery failures are swallowed to keep session-start telemetry non-blocking.

src/agent_scan/hooks/snyk-agent-guard-discover.ps1

snyk-agent-guard-discover.shAdd POSIX session discovery trampoline +10/-0

Add POSIX session discovery trampoline

• Invokes the configured Agent Scan command for best-effort scoped session discovery while preserving stdin for the hook payload.

src/agent_scan/hooks/snyk-agent-guard-discover.sh

snyk-agent-guard.ps1Require machine IDs in PowerShell hook events +11/-2

Require machine IDs in PowerShell hook events

• Accepts machine identity from an argument or environment variable and uses it as the hook event identifier instead of hostname.

src/agent_scan/hooks/snyk-agent-guard.ps1

inspect.pyGate legacy discovery by requested scope +39/-19

Gate legacy discovery by requested scope

• Applies server and skill scopes to well-known clients. Avoids MCP or skill glob work when that discovery half was not requested.

src/agent_scan/inspect.py

pipelines.pyPropagate discovery scope and target folders +31/-5

Propagate discovery scope and target folders

• Extends inspection arguments with scope and target folders. Validates, deduplicates, and forwards hook-provided folders to discoverers without aborting on inaccessible paths.

src/agent_scan/pipelines.py

Bug fix (2) +92 / -8
snyk-agent-guard.shHarden POSIX hook identity and payload transport +4/-3

Harden POSIX hook identity and payload transport

• Requires 'MACHINE_ID' for hook events and uses it as 'X-User.identifier'. Streams encoded payloads to curl over stdin to avoid argument-size limits.

src/agent_scan/hooks/snyk-agent-guard.sh

utils.pyFix home path matching and add TOML escaping +88/-5

Fix home path matching and add TOML escaping

• Makes home abbreviation boundary-aware and Windows case-insensitive. Adds TOML basic-string escaping and unescaping for safe managed Codex hook commands.

src/agent_scan/utils.py

Refactor (2) +15 / -10
antigravity.pyAlign Antigravity discovery root documentation +1/-1

Align Antigravity discovery root documentation

• Updates the Antigravity discoverer documentation to reference the merged discovery-path abstraction.

src/agent_scan/agents/vscode/antigravity.py

verify_api.pyExpose shared backend transport primitives +14/-9

Expose shared backend transport primitives

• Publishes the backend client-session factory and retryable transport exception set so hook delivery shares analysis TLS, proxy, and CA behavior.

src/agent_scan/verify_api.py

Tests (8) +4606 / -320
conftest.pyProvide hermetic discovery command fixtures +13/-0

Provide hermetic discovery command fixtures

• Clears ambient 'AGENT_SCAN_COMMAND' by default and provides the active Agent Scan executable for opt-in hook tests.

tests/conftest.py

test_guard_install.pyVerify discovery events across Guard clients +157/-7

Verify discovery events across Guard clients

• Extends install E2E coverage for Claude, Cursor, and Codex discovery hooks, machine identity, event payloads, durations, and direct 'guard discover' execution.

tests/e2e/test_guard_install.py

test_agent_discovery.pyTest scoped and target-folder discovery +514/-16

Test scoped and target-folder discovery

• Covers discovery-scope gating, merged roots, ancestor handling, symlinks, invalid paths, and explicit-folder discovery across supported agent families and pipelines.

tests/unit/test_agent_discovery.py

test_cli_config_file.pyGuard explicit flag detection across subparsers +33/-8

Guard explicit flag detection across subparsers

• Adds coverage ensuring reused option strings map consistently and explicit CLI flags remain scoped to the active subparser.

tests/unit/test_cli_config_file.py

test_guard.pyCover Guard discovery orchestration and refactors +3592/-289

Cover Guard discovery orchestration and refactors

• Adds broad unit and integration coverage for machine IDs, hook installation, direct discovery events, timeouts, command rendering, scripts, TOML escaping, shared config operations, and failure cleanup.

tests/unit/test_guard.py

test_hook_events.pyTest direct hook-event transport +167/-0

Test direct hook-event transport

• Verifies endpoint selection, wire encoding, machine identity, shared sessions, retry policy, and HTTP or transport failure handling.

tests/unit/test_hook_events.py

test_inspect.pyVerify discovery scope avoids unnecessary scans +82/-0

Verify discovery scope avoids unnecessary scans

• Tests server-only, skill-only, and combined behavior for well-known clients, including elimination of unrequested skill filesystem work.

tests/unit/test_inspect.py

test_utils.pyTest TOML strings and boundary-aware paths +48/-0

Test TOML strings and boundary-aware paths

• Covers TOML escape round trips and home-relative path behavior across boundary, separator, case, and Unicode edge cases.

tests/unit/test_utils.py

Documentation (1) +25 / -3
cli-reference.mdDocument Guard discovery and machine identity requirements +25/-3

Document Guard discovery and machine identity requirements

• Documents the new internal 'guard discover' command, install-time and session-start events, required machine IDs, and 'AGENT_SCAN_COMMAND' behavior. Updates installation examples for the breaking machine-ID requirement.

docs/cli-reference.md

@iamcristi
iamcristi marked this pull request as draft August 20, 2026 09:51
@qodo-merge-etso

qodo-merge-etso Bot commented Aug 20, 2026

Copy link
Copy Markdown

Code Review by Qodo

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

Grey Divider


Action required

1. Machine ID command injection ✓ Resolved 🐞 Bug ⛨ Security
Description
_build_hook_command_powershell embeds the user-controlled --machine-id/MACHINE_ID value in a
single-quoted PowerShell command without escaping apostrophes, allowing a crafted identifier to
terminate the argument and inject PowerShell syntax. The persisted hook command executes when
supported agent hooks run, potentially executing arbitrary commands with the agent user's
privileges.
Code

src/agent_scan/guard.py[1372]

+        command += f" -MachineId '{machine_id}'"
Relevance

●●● Strong

This is a concrete command-injection vulnerability in newly added Windows command construction; no
close rejection precedent outweighs the security impact.

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
The new CLI option accepts an unrestricted string, _run_install forwards it into hook command
construction, and the Windows builder directly interpolates it between apostrophes. Installed client
configurations execute that generated command, while the PowerShell script expects MachineId as an
ordinary parameter, so embedded PowerShell quoting must be escaped before command construction.

src/agent_scan/cli.py[945-955]
src/agent_scan/guard.py[190-193]
src/agent_scan/guard.py[369-376]
src/agent_scan/guard.py[1361-1373]
src/agent_scan/hooks/snyk-agent-guard.ps1[18-24]

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

## Issue description
The generated Windows hook command interpolates `machine_id` into a single-quoted PowerShell argument without escaping embedded apostrophes. A crafted CLI or environment value can break out of the argument and inject PowerShell syntax when an installed hook runs.

## Issue Context
`machine_id` comes from `--machine-id` or `MACHINE_ID` and is persisted in commands installed for Claude, Cursor, and Codex. Introduce a PowerShell-safe argument quoting helper, use it for the new identifier argument, and add regression coverage for apostrophes and command-like content.

## Fix Focus Areas
- src/agent_scan/guard.py[1361-1373]
- tests/unit/test_guard.py[245-253]

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



Remediation recommended

2. Control characters corrupt X-User 🐞 Bug ≡ Correctness ⭐ New
Description
The forwarders interpolate the newly accepted machine ID through JSON escapers that leave several
JSON-forbidden control characters unescaped, so an otherwise nonempty ID containing characters such
as backspace, form feed, or NUL produces an invalid X-User header. On Windows this makes the
mandatory install test event fail and aborts installation; subsequent POSIX hook events can likewise
be rejected.
Code

src/agent_scan/hooks/snyk-agent-guard.ps1[101]

+    (JsonEscape $hostname), (JsonEscape $username), (JsonEscape $MachineId)
Relevance

●●● Strong

Recent accepted precedents favor validating and safely handling malformed or unexpected input
values.

PR-#365
PR-#433

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
The CLI declares machine ID as an unrestricted string and install only strips/rejects emptiness,
while both changed forwarder lines serialize it with helpers that cover backslash, quote, tab, CR,
and LF but omit other JSON-forbidden U+0000–U+001F characters. The PowerShell test event is
mandatory during installation, so malformed header JSON causes _send_test_event to return false
and _install_hooks to abort.

src/agent_scan/cli.py[946-952]
src/agent_scan/guard.py[216-220]
src/agent_scan/hooks/snyk-agent-guard.ps1[90-101]
src/agent_scan/hooks/snyk-agent-guard.sh[32-43]
src/agent_scan/hooks/snyk-agent-guard.sh[130-133]
src/agent_scan/guard.py[554-574]

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

## Issue description
Machine IDs are now inserted into the `X-User` JSON header, but the shell and PowerShell JSON escape helpers do not escape all characters below U+0020. Produce valid JSON for every accepted machine ID, or reject unsupported machine IDs consistently before writing hook configuration.

## Issue Context
`--machine-id` and `MACHINE_ID` accept any nonempty string. Both installed forwarders now pass that value through incomplete hand-written JSON escaping.

## Fix Focus Areas
- src/agent_scan/hooks/snyk-agent-guard.ps1[90-101]
- src/agent_scan/hooks/snyk-agent-guard.sh[32-43]
- src/agent_scan/hooks/snyk-agent-guard.sh[130-133]

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



Informational

3. hook_events exposes exception details 📘 Rule violation ☼ Reliability ⭐ New
Description
The new hook sender catches arbitrary exceptions and returns str(error);
_send_servers_discovered_event then prints that detail to the terminal. Runtime errors may
therefore expose internal URLs, paths, or host information instead of a generic delivery failure.
Code

src/agent_scan/hook_events.py[R48-49]

+        except Exception as error:
+            return False, str(error)
Relevance

●●● Strong

Recent accepted precedent favors concise non-sensitive errors instead of exposing raw exception
details.

PR-#433

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
PR Compliance ID 6 prohibits broad Exception handling that does not re-raise and prohibits
exposing str(e) in user-facing messages. The added sender returns raw exception text at
hook_events.py[48-49], and its only production caller prints the returned detail at
guard.py[1328-1334].

Rule 6: Handle errors explicitly; don't swallow; clean up; don't leak internals
src/agent_scan/hook_events.py[48-49]
src/agent_scan/guard.py[1328-1334]

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

## Issue description
The hook-event sender catches arbitrary exceptions and returns their raw messages, which the guard discovery caller prints to the terminal.

## Issue Context
Compliance rule 6 requires specific exception handling and generic user-facing errors without `str(e)` or other internal details. Preserve useful diagnostics only in appropriately sanitized internal logging.

## Fix Focus Areas
- src/agent_scan/hook_events.py[44-50]
- src/agent_scan/hook_events.py[93-97]
- src/agent_scan/guard.py[1328-1334]

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


4. str(e) reaches terminal ⊘ Outdated 📘 Rule violation ☼ Reliability
Description
_invoke_hook_script catches every exception and returns its raw text, which callers print directly
to the terminal. Exception messages may expose command paths, host details, URLs, or other
implementation internals instead of a generic failure message.
Code

src/agent_scan/guard.py[R1036-1037]

+    except Exception as e:
+        return False, str(e)
Relevance

●● Moderate

Team accepts concise client-facing error handling, but raw-detail leakage precedents are mixed and
not closely matched to this helper.

PR-#214
PR-#433

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
Rule 6 prohibits broad Exception handlers that swallow errors and client-facing messages
containing str(e). The new helper returns str(e) at lines 1036-1037, while event handlers print
that detail at lines 1082 and 1117; the discovery handler independently prints its caught exception
at lines 1096-1097.

Rule 6: Handle errors explicitly; don't swallow; clean up; don't leak internals
src/agent_scan/guard.py[1034-1037]
src/agent_scan/guard.py[1078-1082]
src/agent_scan/guard.py[1094-1097]
src/agent_scan/guard.py[1111-1117]

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

## Issue description
Broad exception handling returns `str(e)`, which is subsequently displayed to users and may expose internal details.

## Issue Context
Compliance rule 6 requires specific exception handling and generic client-facing error messages. Preserve detailed diagnostics only in appropriately protected logs.

## Fix Focus Areas
- src/agent_scan/guard.py[1034-1037]
- src/agent_scan/guard.py[1078-1082]
- src/agent_scan/guard.py[1094-1097]
- src/agent_scan/guard.py[1111-1117]

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


5. _invoke_hook_script exceeds 40 SLOC ⊘ Outdated 📘 Rule violation ☼ Reliability
Description
The new _invoke_hook_script spans 51 nonblank source lines and combines platform-specific command
construction, environment preparation, process execution, and error translation. This exceeds the
checklist's 40-line limit and makes each platform path harder to maintain independently.
Code

src/agent_scan/guard.py[R987-990]

+def _invoke_hook_script(
+    script_path: Path,
+    hook_client: str,
+    push_key: str,
Relevance

● Weak

Recent guard.py precedents explicitly rejected 40-SLOC refactoring requests, including orchestration
and substantial helper functions.

PR-#391
PR-#388

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
Rule 45 limits functions to at most 40 SLOC. The newly added function occupies lines 987-1037 and
includes both platform dispatch and subprocess lifecycle handling.

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

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

## Issue description
`_invoke_hook_script` exceeds 40 SLOC and handles several distinct responsibilities.

## Issue Context
Extract Windows and POSIX command/environment construction into clearly named helpers, leaving `_invoke_hook_script` responsible for process execution and result handling.

## Fix Focus Areas
- src/agent_scan/guard.py[987-1037]

ⓘ 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 reply 'qodo' on any finding to push back, ask questions, or dig deeper

More tips ↗ | Customize Qodo ↗ | Qodo docs ↗

Grey Divider

Previous reviews

Review updated until commit 3ef9db1

Results up to commit 0eeed88 ⚖️ Balanced


🐞 Bugs (0) 📘 Rule violations (0) 📎 Requirement gaps (0) 🎨 UX issues (0) 🔗 Cross-repo conflicts (0) 📜 Skill insights (0)


Action required
1. Machine ID command injection ✓ Resolved 🐞 Bug ⛨ Security
Description
_build_hook_command_powershell embeds the user-controlled --machine-id/MACHINE_ID value in a
single-quoted PowerShell command without escaping apostrophes, allowing a crafted identifier to
terminate the argument and inject PowerShell syntax. The persisted hook command executes when
supported agent hooks run, potentially executing arbitrary commands with the agent user's
privileges.
Code

src/agent_scan/guard.py[1372]

+        command += f" -MachineId '{machine_id}'"
Relevance

●●● Strong

This is a concrete command-injection vulnerability in newly added Windows command construction; no
close rejection precedent outweighs the security impact.

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
The new CLI option accepts an unrestricted string, _run_install forwards it into hook command
construction, and the Windows builder directly interpolates it between apostrophes. Installed client
configurations execute that generated command, while the PowerShell script expects MachineId as an
ordinary parameter, so embedded PowerShell quoting must be escaped before command construction.

src/agent_scan/cli.py[945-955]
src/agent_scan/guard.py[190-193]
src/agent_scan/guard.py[369-376]
src/agent_scan/guard.py[1361-1373]
src/agent_scan/hooks/snyk-agent-guard.ps1[18-24]

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

## Issue description
The generated Windows hook command interpolates `machine_id` into a single-quoted PowerShell argument without escaping embedded apostrophes. A crafted CLI or environment value can break out of the argument and inject PowerShell syntax when an installed hook runs.

## Issue Context
`machine_id` comes from `--machine-id` or `MACHINE_ID` and is persisted in commands installed for Claude, Cursor, and Codex. Introduce a PowerShell-safe argument quoting helper, use it for the new identifier argument, and add regression coverage for apostrophes and command-like content.

## Fix Focus Areas
- src/agent_scan/guard.py[1361-1373]
- tests/unit/test_guard.py[245-253]

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



Informational
2. str(e) reaches terminal ⊘ Outdated 📘 Rule violation ☼ Reliability
Description
_invoke_hook_script catches every exception and returns its raw text, which callers print directly
to the terminal. Exception messages may expose command paths, host details, URLs, or other
implementation internals instead of a generic failure message.
Code

src/agent_scan/guard.py[R1036-1037]

+    except Exception as e:
+        return False, str(e)
Relevance

●● Moderate

Team accepts concise client-facing error handling, but raw-detail leakage precedents are mixed and
not closely matched to this helper.

PR-#214
PR-#433

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
Rule 6 prohibits broad Exception handlers that swallow errors and client-facing messages
containing str(e). The new helper returns str(e) at lines 1036-1037, while event handlers print
that detail at lines 1082 and 1117; the discovery handler independently prints its caught exception
at lines 1096-1097.

Rule 6: Handle errors explicitly; don't swallow; clean up; don't leak internals
src/agent_scan/guard.py[1034-1037]
src/agent_scan/guard.py[1078-1082]
src/agent_scan/guard.py[1094-1097]
src/agent_scan/guard.py[1111-1117]

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

## Issue description
Broad exception handling returns `str(e)`, which is subsequently displayed to users and may expose internal details.

## Issue Context
Compliance rule 6 requires specific exception handling and generic client-facing error messages. Preserve detailed diagnostics only in appropriately protected logs.

## Fix Focus Areas
- src/agent_scan/guard.py[1034-1037]
- src/agent_scan/guard.py[1078-1082]
- src/agent_scan/guard.py[1094-1097]
- src/agent_scan/guard.py[1111-1117]

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


3. _invoke_hook_script exceeds 40 SLOC ⊘ Outdated 📘 Rule violation ☼ Reliability
Description
The new _invoke_hook_script spans 51 nonblank source lines and combines platform-specific command
construction, environment preparation, process execution, and error translation. This exceeds the
checklist's 40-line limit and makes each platform path harder to maintain independently.
Code

src/agent_scan/guard.py[R987-990]

+def _invoke_hook_script(
+    script_path: Path,
+    hook_client: str,
+    push_key: str,
Relevance

● Weak

Recent guard.py precedents explicitly rejected 40-SLOC refactoring requests, including orchestration
and substantial helper functions.

PR-#391
PR-#388

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
Rule 45 limits functions to at most 40 SLOC. The newly added function occupies lines 987-1037 and
includes both platform dispatch and subprocess lifecycle handling.

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

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

## Issue description
`_invoke_hook_script` exceeds 40 SLOC and handles several distinct responsibilities.

## Issue Context
Extract Windows and POSIX command/environment construction into clearly named helpers, leaving `_invoke_hook_script` responsible for process execution and result handling.

## Fix Focus Areas
- src/agent_scan/guard.py[987-1037]

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


Grey Divider

Qodo Logo

Comment thread src/agent_scan/guard.py
Comment thread src/agent_scan/guard.py Outdated
_run_install tests that mock _install_hooks were running the real
post-install serversDiscovered send: actual machine discovery plus a
hook-script invocation on a mock path (~1.2s per test, environment-
dependent). Patch _send_servers_discovered_event autouse in the three
affected classes, and pin that clients with only unparseable configs
still emit an empty-servers entry.
_copy_hook_script returned was_updated=False whenever the forwarding
script was already current, so an install that only restored or updated
snyk-agent-guard-discover.sh printed 'hook integration up to date'.
Track the discovery-script write and include it in the returned flag.
@iamcristi
iamcristi marked this pull request as ready for review August 27, 2026 06:18
Comment thread src/agent_scan/hook_events.py
Comment thread src/agent_scan/hooks/snyk-agent-guard.ps1
@qodo-merge-etso

Copy link
Copy Markdown

Code review by qodo was updated up to the latest commit 4c32484

Comment thread src/agent_scan/guard.py
@eugene-eko2000

Copy link
Copy Markdown
Contributor

Consider omitted protocol in the url, use http:// if omitted.

guard install accepted --control-identifier as an alias for --machine-id,
which made that option string mean control_identifier on scan/inspect/evo
and machine_id here. explicitly_provided_dests keys a flat option-string to
dest map, so the collision resolved to whichever action the walk visited
last -- scan --config-file c.yaml --control-server URL --control-identifier
ID stopped registering control_identifier as explicit and let the config
file's control_servers override the command line.

Keep --machine-id only. --control-identifier is already deprecated in
favour of it, and guard install could never warn about the spelling because
its dest is machine_id. explicitly_provided_dests goes back to walking
every action, and a test over the real parser now asserts no option string
maps to two dests.
The `--` break and its test are unrelated to guard server discovery. The
argv scan mistaking a post-`--` positional for a flag predates this branch,
so the fix belongs on its own change against main rather than riding along
here.
Reverts wording churn and an invariant note that duplicated the enforcing
test's own docstring. The function now matches main verbatim.
The install failure path snapshotted each hook script's bytes and mode
before the copy and wrote them back when the test event failed, covering
scripts that merely got overwritten. main only deletes a script the install
had just created, so this widened the abort contract well past the PR's
scope.

Back to main's rule, applied to both scripts: a newly created forwarder or
discovery trampoline is unlinked on abort, a pre-existing one is left alone.
Removes _HookScriptBackup, _snapshot_hook_script, _restore_hook_script and
_hook_script_path, whose only purpose was the snapshot.
The Python delivery path claimed to be snyk-agent-guard.sh/.ps1, so backend
telemetry could not tell shell-script traffic apart from in-process sends.
Identify the actual sender instead.
_agent_scan_bin guessed the CLI location from sys.frozen, argv[0] and the
interpreter's sibling console script when AGENT_SCAN_BIN was unset. The
caller supplies the path instead, so the guessing is gone: the value is read
from the environment at install time and baked into the hook command as
before, and an unset variable leaves it out so the trampolines fall back to
PATH.
The four command builders were a platform x variant cross-product, each
re-implementing the same platform logic, and _invoke_hook_script described the
same contract a fifth time as an argv list plus env dict with no shared code.
Adding a field meant four coordinated edits and a fifth in a subprocess path;
tenant_id had already drifted, accepted by all four builders but emitted by one.

A single _HookInvocation now holds the raw values and three renderers turn it
into a POSIX shell string, a PowerShell string, or an argv/env pair. Skipping
empty optional fields is what lets one code path reproduce both variants, so the
strings written into agent config files are unchanged: verified byte-identical
across 5832 input combinations covering both platforms and embedded apostrophes.
That matters because the emitted command is parsed back by _DETECTION_RE,
_extract_env_from_cmd and the push-key redaction patterns, all of which depend on
argument order and single-quoting.

Two per-variant quirks stay encoded in the spec rather than normalised: the main
forwarder leaves --client unquoted where discovery quotes it, and TENANT_ID is
emitted only on the POSIX main path, where _parse_command_info reads it back as
install-time metadata.

_build_discover_hook_command_powershell is gone. Routing the Windows discovery
path straight at the shared renderer left it unreferenced, and its output is
reproduced exactly by the surviving path. _build_hook_command_powershell stays,
since the live subprocess round-trip calls it directly on any platform.

Tests pin the exact output of every builder per platform, so a reordering that
_DETECTION_RE still happens to match can no longer slip through, and cover the
discovery-shaped argv on both platforms.
An unset variable leaves the value out of the hook command so the trampolines
fall back to PATH, and a bare executable name resolves through PATH even when
the variable is set, so the trampoline does not bypass the lookup.
_uninstall_hooks took a missing_label argument used only in the
"Nothing to uninstall" message. path.name yields the same string for
every caller and is accurate for managed and --file paths, where the
hardcoded label named a file that was never checked.
The posix _render_argv tests force IS_WINDOWS=False but built the script
path with Path(), whose flavour follows the host. On the Windows runner
that stringifies with backslashes, so the hardcoded POSIX expectation
failed. Compare against str(script_path) instead, matching the sibling
Windows test.
@iamcristi
iamcristi disabled auto-merge August 28, 2026 07:25
@iamcristi
iamcristi force-pushed the feat/guard-serversdiscovered-event branch from 70eead6 to 3ef9db1 Compare August 28, 2026 07:26
@iamcristi
iamcristi merged commit bf1cde9 into main Aug 28, 2026
11 checks passed
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.

3 participants