Skip to content

Hook to enforce ★ Insight → event bus publish rule #301

Description

@evansenter

Problem

The global CLAUDE.md contains a rule:

★ Insight → Event Bus rule: Every ★ Insight block MUST be immediately followed by a publish_event call in the same response.

In practice, Claude frequently emits ★ Insight blocks in the explanatory output style and does not publish them. This has been observed repeatedly across sessions — it's a soft rule the assistant consistently forgets, so cross-session insight capture is lossy.

CLAUDE.md is advisory; only hooks are enforced by the harness. So the fix is a hook.

Proposal

Add a Stop hook ~/.claude/hooks/enforce-insight-publish.sh that:

  1. Reads stdin JSON → extracts transcript_path and stop_hook_active
  2. If stop_hook_active is true, exits 0 (prevents infinite loop)
  3. Parses the JSONL transcript back to the last user event
  4. Counts ★ Insight markers in assistant text content since then
  5. Counts mcp__agent-event-bus__publish_event tool_use blocks since then
  6. If insights > 0 and publishes == 0, returns JSON: {"decision": "block", "reason": "You emitted N ★ Insight block(s) but made no publish_event calls. Per global CLAUDE.md, publish each insight to the event bus before ending the turn."}
  7. Otherwise exits 0

The decision: block return feeds reason back to Claude as a continuation prompt, forcing a publish before the turn actually ends.

Wire-up

Append to the existing Stop array in home/.claude/settings.json:

"Stop": [{
  "hooks": [
    {"type": "command", "command": "~/.claude/hooks/zj-status.sh waiting"},
    {"type": "command", "command": "~/.claude/hooks/enforce-insight-publish.sh"}
  ]
}]

Trade-offs

  • False positives — quoting the rule itself, referencing an ★ Insight in a discussion, or pasting someone else's transcript could trigger it. Mitigation: match the full decorated form (★ Insight ─────), not bare text.
  • Lenient vs strict counting — lenient (publishes >= 1 when insights >= 1) lets batches of insights share a publish; strict (publishes >= insights) is more faithful to the rule. Start lenient, tighten if Claude games it.
  • Infinite loop risk — mitigated by the stop_hook_active check. If Claude fails to publish after the block, the next Stop will see stop_hook_active=true and let the turn end.

Acceptance criteria

  • home/.claude/hooks/enforce-insight-publish.sh exists, is executable, uses set -euo pipefail, consumes stdin
  • Gracefully degrades when jq is missing (exits 0)
  • Added to Stop array in home/.claude/settings.json
  • Documented in home/.claude/hooks/README.md (new section + updated lifecycle diagram)
  • Test added to tests/test-hooks.sh (graceful degradation)
  • make check passes

Generalizability

This pattern — Stop hook parses transcript, validates a MUST-rule, returns decision: block if violated — is reusable for any "assistant MUST do X every turn" rule in CLAUDE.md. Worth keeping the script structure modular so future rules can be added as separate hooks or as a unified validator.

Context

  • Discussion: session 25ccf498-fb39-4235-b14b-aa75160b4cef on 2026-04-21
  • Event bus improvement_suggested event 3875
  • Template: existing home/.claude/hooks/post-tool-failure.sh demonstrates transcript-aware hook patterns

Metadata

Metadata

Assignees

No one assigned

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions