Problem
scripts/hooks/web_tools_gate.py is advisory-only (exit 0 always, once-per-session sentinel). Measured in practice: the nudge was delivered alongside a parallel batch of built-in WebSearch calls and did not change behavior — PreToolUse advisory text arrives with the tool results, after execution, so a once-per-session nudge cannot prevent the first offense and a feature-list message reads as FYI.
Design (fully planned; plan file exists locally)
Escalate to a deny with a blackout-then-grace window, per session:
| Elapsed since block_start |
Verdict |
| (no timestamp) |
DENY, set block_start=now |
| < 10s (blackout) |
DENY — catches all siblings of a parallel batch |
| 10s…30min (grace) |
ALLOW — the retry IS the override |
| >= 30min |
DENY, new window |
Key points:
- Deny =
BLOCKED: on stderr + exit 2 (house pattern; proven on non-Bash matchers by the inline WebFetch hook in settings.json, tests in tests/test_hooks/test_inline_hooks.py).
- Do NOT wrap in
hook_input.run_guard — that wrapper is fail-closed; its own docstring restricts it to guards where fail-closed is correct. This is a convenience guard: fail-open on any OSError/malformed payload/unsafe session id.
WebFetch exemption for claude.ai URLs (CC's WebFetch is the only tool that can read claude.ai artifacts).
- State: mtime of a per-session file under
~/.genesis/sessions/<sid>/ via hook_input.session_path() — NOT tempdir (cc-tmp is watchgod-policed).
- Message must name the CLAUDE.md rule (default to MCP web tools), state the narrow-lookup carve-out, and say the retry-after-blackout override plainly — a block the agent reads as broken gets escalated around.
tests/test_hooks/test_advisory_channel.py::test_web_tools_gate_wrapped asserts returncode 0 and must be re-pointed at the grace path.
- Test matrix: {WebSearch, WebFetch, WebFetch@claude.ai} x {no state, blackout, grace, expired} x {valid, malformed, unreadable, unsafe sid}. Verify-RED each cell. Age via
os.utime, subprocess with fresh TMPDIR/HOME (existing conventions).
- Worktree already exists:
.claude/worktrees/web-tools-block (branch fix/web-tools-gate-block, based on origin/main, untouched).
- NOT covered by update_hook_versions.sh / sync-hooks.sh (CC hook, not git hook — verified). Live only for new CC sessions; note mid-window in PR.
Classification (per hook-design axioms)
Advisory→block escalation was explicitly owner-directed after discussion (measured trigger: a correctly-delivered advisory ignored on the rule it enforces). Audience: agent. Background: blocks fg/bg equally; bg self-overrides by re-issuing — a block, not an ask.
Problem
scripts/hooks/web_tools_gate.pyis advisory-only (exit 0 always, once-per-session sentinel). Measured in practice: the nudge was delivered alongside a parallel batch of built-inWebSearchcalls and did not change behavior — PreToolUse advisory text arrives with the tool results, after execution, so a once-per-session nudge cannot prevent the first offense and a feature-list message reads as FYI.Design (fully planned; plan file exists locally)
Escalate to a deny with a blackout-then-grace window, per session:
Key points:
BLOCKED:on stderr +exit 2(house pattern; proven on non-Bash matchers by the inline WebFetch hook in settings.json, tests intests/test_hooks/test_inline_hooks.py).hook_input.run_guard— that wrapper is fail-closed; its own docstring restricts it to guards where fail-closed is correct. This is a convenience guard: fail-open on any OSError/malformed payload/unsafe session id.WebFetchexemption for claude.ai URLs (CC's WebFetch is the only tool that can read claude.ai artifacts).~/.genesis/sessions/<sid>/viahook_input.session_path()— NOT tempdir (cc-tmp is watchgod-policed).tests/test_hooks/test_advisory_channel.py::test_web_tools_gate_wrappedasserts returncode 0 and must be re-pointed at the grace path.os.utime, subprocess with fresh TMPDIR/HOME (existing conventions)..claude/worktrees/web-tools-block(branchfix/web-tools-gate-block, based on origin/main, untouched).Classification (per hook-design axioms)
Advisory→block escalation was explicitly owner-directed after discussion (measured trigger: a correctly-delivered advisory ignored on the rule it enforces). Audience: agent. Background: blocks fg/bg equally; bg self-overrides by re-issuing — a block, not an ask.