Skip to content

feat(hooks): escalate web_tools_gate from advisory nudge to rate-limited block #1582

Description

@WingedGuardian

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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions