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:
- Reads stdin JSON → extracts
transcript_path and stop_hook_active
- If
stop_hook_active is true, exits 0 (prevents infinite loop)
- Parses the JSONL transcript back to the last
user event
- Counts
★ Insight markers in assistant text content since then
- Counts
mcp__agent-event-bus__publish_event tool_use blocks since then
- 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."}
- 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
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
Problem
The global CLAUDE.md contains a rule:
In practice, Claude frequently emits
★ Insightblocks 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
Stophook~/.claude/hooks/enforce-insight-publish.shthat:transcript_pathandstop_hook_activestop_hook_activeistrue, exits 0 (prevents infinite loop)userevent★ Insightmarkers in assistant text content since thenmcp__agent-event-bus__publish_eventtool_use blocks since then{"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."}The
decision: blockreturn feedsreasonback 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:Trade-offs
★ Insightin a discussion, or pasting someone else's transcript could trigger it. Mitigation: match the full decorated form (★ Insight ─────), not bare text.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.stop_hook_activecheck. If Claude fails to publish after the block, the next Stop will seestop_hook_active=trueand let the turn end.Acceptance criteria
home/.claude/hooks/enforce-insight-publish.shexists, is executable, usesset -euo pipefail, consumes stdinjqis missing (exits 0)Stoparray inhome/.claude/settings.jsonhome/.claude/hooks/README.md(new section + updated lifecycle diagram)tests/test-hooks.sh(graceful degradation)make checkpassesGeneralizability
This pattern — Stop hook parses transcript, validates a MUST-rule, returns
decision: blockif 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
25ccf498-fb39-4235-b14b-aa75160b4cefon 2026-04-21improvement_suggestedevent3875home/.claude/hooks/post-tool-failure.shdemonstrates transcript-aware hook patterns