Hook BLOCK/REMINDER/WARN decisions have no durable sink — only ephemeral stderr
Severity: medium | Confidence: 0.7 | Effort: M
Where:
plugins/ca/hooks/_hooklib.py:596-609
Evidence: block()/remind()/warn() (the three primitives every one of the 16 entry hooks funnels every gate decision through, 21 consumers of _hooklib.py) each do nothing but print(..., file=sys.stderr) (block also sys.exit(2)). No call anywhere in plugins/ca/hooks writes a gate decision (which tag fired, block vs remind vs degrade-warn, timestamp) to any file. The only durable .codearbiter logs are the H-05 audit set (overrides.log, triage.log, sprint-log.md), which record /override and ADR actions that Claude itself writes in prose, not the mechanical hook decision stream.
Impact: Every BLOCKED/REMINDER/degradation event is visible only in the live transcript for that one turn. There is no way to answer, after the fact or across a session/repo, "how often did H-09b fire", "which gates degraded silently (warn()) this week", or "did a block get worked around by a retried tool call" without scraping transcripts. This weakens the audit-completeness story the plugin sells (gates-as-enforcement) for the mechanical gate layer, distinct from the already-open /override log-write-compulsion question (CONFIRM-09) which concerns only the prose-driven override log.
Recommendation: Add an optional, best-effort append to a dedicated .codearbiter/gate-events.log (or reuse the H-05 audit-log machinery) from block()/remind()/warn(), gated so it never turns a fail-open hook into a fail-closed one on write failure, mirroring the fail-open discipline already used throughout _hooklib.py.
Acceptance criteria:
- A repo-local run of a hook that hits block() or warn() produces a corresponding durable, greppable record outside the live transcript
- The added write path is proven fail-open (a locked/missing log file does not change the hook's exit code)
Hook BLOCK/REMINDER/WARN decisions have no durable sink — only ephemeral stderr
Severity: medium | Confidence: 0.7 | Effort: M
Where:
plugins/ca/hooks/_hooklib.py:596-609
Evidence: block()/remind()/warn() (the three primitives every one of the 16 entry hooks funnels every gate decision through, 21 consumers of _hooklib.py) each do nothing but
print(..., file=sys.stderr)(block alsosys.exit(2)). No call anywhere in plugins/ca/hooks writes a gate decision (which tag fired, block vs remind vs degrade-warn, timestamp) to any file. The only durable .codearbiter logs are the H-05 audit set (overrides.log, triage.log, sprint-log.md), which record /override and ADR actions that Claude itself writes in prose, not the mechanical hook decision stream.Impact: Every BLOCKED/REMINDER/degradation event is visible only in the live transcript for that one turn. There is no way to answer, after the fact or across a session/repo, "how often did H-09b fire", "which gates degraded silently (warn()) this week", or "did a block get worked around by a retried tool call" without scraping transcripts. This weakens the audit-completeness story the plugin sells (gates-as-enforcement) for the mechanical gate layer, distinct from the already-open /override log-write-compulsion question (CONFIRM-09) which concerns only the prose-driven override log.
Recommendation: Add an optional, best-effort append to a dedicated .codearbiter/gate-events.log (or reuse the H-05 audit-log machinery) from block()/remind()/warn(), gated so it never turns a fail-open hook into a fail-closed one on write failure, mirroring the fail-open discipline already used throughout _hooklib.py.
Acceptance criteria: