Repository navigation
Conversation
A forwarder request to agent-monitor that hangs instead of failing (a proxy or middlebox that completes the handshake and then goes silent) stalls the agent for the hook's default timeout on every event: 600 s on Claude Code and Codex, an undocumented platform default on Cursor. Every agent treats a timed-out hook as fail-open, so an explicit cap bounds the stall without changing enforcement. Per client: - Claude Code: `timeout: 30` on every guard entry except SessionEnd, whose 1.5 s shared budget Claude Code raises to match a longer per-hook timeout. - Codex: `timeout: 30` on all seven guard entries in hooks.json and a `timeout = 30` line in each guard table of the managed requirements.toml. The TOML parser now reads the guard timeout back, so a managed install written before this change is reported as modified on reinstall, matching what the JSON clients already get from dict comparison. - Cursor: `timeout: 30` on every guard entry except sessionStart and sessionEnd, which Cursor documents as fire-and-forget. - Copilot: `timeoutSec: 30` on every entry, including the SessionStart discovery entry, since Copilot has no async hooks. 30 s is already its default; writing it keeps the bound explicit. The async discovery entries on Claude Code and Codex are unchanged: Claude Code does not enforce `timeout` on async hooks, and the discovery CLI bounds itself.
PR Summary by QodoCap synchronous Agent Guard hooks at 30 seconds
AI Description
Diagram
High-Level Assessment
Files changed (2)
|
Code Review by Qodo
1. Some hook timeout changes go unreported
|
| actual = ( | ||
| _entries(event, old_guard_command, old_discover_command, old_guard_timeout) | ||
| if event in old_event_set | ||
| else [] |
There was a problem hiding this comment.
1. Some hook timeout changes go unreported 🐞 Bug ≡ Correctness
_parse_codex_requirements_toml retains the timeout from only the first guard table, and _prepare_codex_managed_config uses it as the actual timeout for every event. If a managed Codex file has a 30-second timeout on its first guard hook but a missing or different timeout on a later one, reinstall rewrites that later hook without reporting it as modified.
Agent Prompt
## Issue description
The managed Codex parser returns one timeout for all guard hooks, so the reinstall diff misses changes to later hooks whose timeouts differ from the first.
## Fix Focus Areas
- src/agent_scan/guard.py[1018-1063]
- src/agent_scan/guard.py[970-975]
- tests/unit/test_guard.py[3255-3274]
## Recommended Fix
Parse each guard hook's timeout alongside its event and command, then compare each event's actual entry against its expected entry. Add a test that removes or changes the timeout on only one later event.
ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools
| table_is_guard = False | ||
| header_re = re.compile(r"^\[\[hooks\.([A-Za-z]+)(?:\.hooks)?\]\]\s*$") | ||
| command_re = re.compile(r'^command\s*=\s*"((?:[^"\\]|\\.)*)"\s*$') | ||
| timeout_re = re.compile(r"^timeout\s*=\s*(\d+)\s*$") |
There was a problem hiding this comment.
2. Valid timeouts appear missing on reinstall 🐞 Bug ≡ Correctness
_parse_codex_requirements_toml recognizes timeout only when its value consists of bare decimal digits followed immediately by the end of the line. If an existing managed Codex file uses valid TOML such as timeout = 30 # guard timeout or timeout = +30, reinstall reports the hook as modified even though its configured timeout is already 30 seconds.
Agent Prompt
## Issue description
The new timeout regex treats valid TOML integer forms and inline comments as an absent timeout, producing an inaccurate managed-install diff.
## Fix Focus Areas
- src/agent_scan/guard.py[1023-1046]
- tests/unit/test_guard.py[3237-3274]
## Recommended Fix
Read the existing timeout using TOML parsing, or support valid TOML integer syntax and inline comments in the scoped table parser. Test a managed file whose timeout has an inline comment and one using a signed integer.
ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools
agent-monitor holds a PreToolUse for up to 25 s until the session's discovery arrives, then evaluates within its 25 s autorelease budget, so a legitimate verdict can take ~50 s. At 27 s the scripts gave up on the first MCP calls of a session, exactly where enforcement matters most. The lower bound the budget test pins moves from 25 s to 50 s. The upper bound against the agent-side cap is asserted next to that cap (#511), which moves to 65 s.
The forwarder scripts now give the request 60 s (#512), because agent-monitor may legitimately take ~50 s to answer. A 30 s cap would kill the hook before the script could fail open with its diagnostic, and below Copilot's 30 s default the explicit value was redundant. A new test asserts the scripts' TIMEOUT_SECONDS stays under the cap, skipping until the forwarder budget is on the same branch.
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes using default effort and found 1 potential issue.
❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, have a team admin enable autofix in the Cursor dashboard.
Reviewed by Cursor Bugbot for commit 3cc7ec5. Configure here.
| # event. Each agent treats a timed-out hook as fail-open. The value sits above the | ||
| # forwarder scripts' 60 s request budget, so a hung request fails open inside the | ||
| # script, with its diagnostic, before the agent kills the hook. | ||
| HOOK_TIMEOUT_SECONDS = 65 |
There was a problem hiding this comment.
Timeout raised without script request cap
Medium Severity
HOOK_TIMEOUT_SECONDS is now 65 to outlast a 60 s forwarder budget, but snyk-agent-guard.sh and snyk-agent-guard.ps1 still call curl and Invoke-WebRequest with no request timeout. Copilot's timeoutSec and Claude Code's UserPromptSubmit 30 s defaults are both raised, so a hung request stalls longer and the script never fails open with a diagnostic.
Additional Locations (2)
Reviewed by Cursor Bugbot for commit 3cc7ec5. Configure here.


Why
The hook forwarder scripts call agent-monitor with no request timeout (
Invoke-WebRequestwithout-TimeoutSec, curl without--max-time). A request that hangs instead of failing, which is what a TLS-intercepting proxy, captive portal or middlebox that completes the handshake and goes silent produces, stalls the agent for the hook's default timeout on every event:timeouttimeouttimeouttimeoutSecOn Claude Code that is up to 10 minutes before a tool runs and 10 more after it, per tool call. Every agent documents a timed-out hook as fail-open, so an explicit cap bounds the stall without changing enforcement.
What changes
guard installnow writes a 65 s timeout (HOOK_TIMEOUT_SECONDS) on every synchronous guard hook entry. 65 s sits above the forwarder scripts' 60 s request budget (#512), so a hung request fails open inside the script, with its diagnostic, before the agent kills the hook:"timeout": 65on every guard entry exceptSessionEnd. Claude Code's SessionEnd hooks share a 1.5 s budget that it raises to match a longer per-hooktimeout, so a value there would delay every exit by 65 s instead of bounding a hang."timeout": 65on all seven guard entries inhooks.json, andtimeout = 65in each guard table of the managedrequirements.toml._parse_codex_requirements_tomlnow returns the guard timeout as a fourth value so a managed install written before this change is reported as modified on reinstall, matching what the JSON clients get from plain dict comparison."timeout": 65on every guard entry exceptsessionStartandsessionEnd, which Cursor documents as fire-and-forget ("the agent loop does not wait for or enforce a blocking response")."timeoutSec": 65on every entry, including the SessionStart discovery entry. Copilot has no async hooks, so that entry is synchronous too. Copilot's default is 30 s, below the scripts' 60 s budget, so the explicit value is what lets the forwarder fail open on its own terms there.Unchanged: the async discovery entries on Claude Code and Codex (Claude Code does not enforce
timeoutonasync: truehooks, andguard discoverbounds itself at 5 s stdin / 60 s discovery / 15 s request), the hook scripts, detection and uninstall.Existing installs will show the timeout as a
modifiedhooks diff on their nextguard install, which is the intended signal.Docs
timeoutfield and SessionEnd budget: https://code.claude.com/docs/en/hookstimeoutdefault 600: https://learn.chatgpt.com/docs/hookstimeout, fail-open on timeout: https://cursor.com/docs/agent/hookstimeoutSecdefault 30, timeouts fail-open: https://docs.github.com/en/copilot/reference/hooks-configurationCaveat worth a manual check
Codex's docs do not spell out what a timed-out synchronous
PreToolUse/PermissionRequestcommand hook does to the tool call. The only concrete sentence ("aPreToolUsecallback error, timeout, or malformed response can fail the hook without blocking the tool") is in the managed MCP hooks section. Claude Code, Cursor and Copilot are explicit that a timeout does not block. Worth confirming on a real Codex install before relying on it.Verification
tests/unit/test_guard.py(TestSyncHookTimeoutplus four Codex TOML cases) were red before the change and are green after; 5 existing exact-shape assertions and 4 parser call sites updated.origin/main(89 entries, all environmental: e2e[binary]params withoutdist/,SNYK_TOKEN, and the managed-install tests that hit the real/Library/Application Support/ClaudeCode). Passed went from 2381 to 2395, exactly the new tests.pre-commit run --all-filesclean; local ruff 0.15 lint and format also clean on both changed files; mypy reports no errors inguard.py.A follow-up should add a request-level timeout to the scripts themselves (
-TimeoutSec/--max-time), which is the primary control; this PR is the agent-side backstop.Note
Medium Risk
Touches hook config for all supported agents on every reinstall; mis-tuned timeouts could change stall/fail-open behavior, though changes are bounded and documented as fail-open on timeout.
Overview
guard installnow writes a 65 s agent-side hook cap (HOOK_TIMEOUT_SECONDS) so hung forwarder requests cannot block Claude/Codex/Cursor for hundreds of seconds per event. The value is intentionally above the forwarder scripts’ ~60 s request budget so the script can fail open with diagnostics before the agent kills the hook.Per-client behavior: Claude gets
timeout: 65on all guard hooks exceptSessionEnd(avoids stretching the shared 1.5 s exit budget). Cursor skipssessionStart/sessionEnd(documented fire-and-forget). Codex addstimeoutin userhooks.jsonand managedrequirements.toml, with_parse_codex_requirements_tomlextended to read/write timeout so older installs show as modified on reinstall. Copilot setstimeoutSec: 65on every entry, including session discovery (no async hooks).Unit tests add
TestSyncHookTimeoutand update shape/diff expectations; async discovery hooks remain without these timeouts.Reviewed by Cursor Bugbot for commit d20c075. Bugbot is set up for automated code reviews on this repo. Configure here.