From 33b06e5ec66e1dd2eefcf128f61aa4020680b3d7 Mon Sep 17 00:00:00 2001 From: WingsOfPanda <46889518+WingsOfPanda@users.noreply.github.com> Date: Wed, 15 Apr 2026 18:03:47 +0800 Subject: [PATCH 1/2] fix(hookify): pass warn/block messages to Claude MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Warn rules previously returned only `systemMessage`, which is shown to the user console but never injected into Claude's context. Educational messages (the entire point of warn rules) never reached the model. Block rules similarly returned only a generic deny without `permissionDecisionReason`. Now: - Warn rules on PreToolUse/PostToolUse also emit `additionalContext` inside `hookSpecificOutput`, so Claude sees the message and can self-correct. - Block rules on PreToolUse/PostToolUse also emit `permissionDecisionReason`, so Claude sees why the operation was denied. PreToolUse `additionalContext` support landed in Claude Code v2.1.9 (see anthropics/claude-code#15664, anthropics/claude-code#15345). PostToolUse has supported it since earlier. The capability is now available; this patch makes hookify use it. Refs: - anthropics/claude-code#15203 (warn — closed as duplicate) - anthropics/claude-code#12446 (block — closed) - anthropics/claude-code#15664, anthropics/claude-code#15345 (SDK additionalContext support) - Prior PR attempts: anthropics/claude-code#15218, anthropics/claude-code#15219 (closed unmerged, predate v2.1.9) --- plugins/hookify/core/rule_engine.py | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/plugins/hookify/core/rule_engine.py b/plugins/hookify/core/rule_engine.py index 51561c39e..245be82b7 100644 --- a/plugins/hookify/core/rule_engine.py +++ b/plugins/hookify/core/rule_engine.py @@ -73,7 +73,8 @@ def evaluate_rules(self, rules: List[Rule], input_data: Dict[str, Any]) -> Dict[ return { "hookSpecificOutput": { "hookEventName": hook_event, - "permissionDecision": "deny" + "permissionDecision": "deny", + "permissionDecisionReason": combined_message, }, "systemMessage": combined_message } @@ -86,9 +87,17 @@ def evaluate_rules(self, rules: List[Rule], input_data: Dict[str, Any]) -> Dict[ # If only warnings, show them but allow operation if warning_rules: messages = [f"**[{r.name}]**\n{r.message}" for r in warning_rules] - return { - "systemMessage": "\n\n".join(messages) - } + combined = "\n\n".join(messages) + response = {"systemMessage": combined} + # PreToolUse additionalContext supported since Claude Code v2.1.9. + # PostToolUse has supported it since earlier. Inject so Claude (not + # just the user console) sees the educational message. + if hook_event in ('PreToolUse', 'PostToolUse'): + response["hookSpecificOutput"] = { + "hookEventName": hook_event, + "additionalContext": combined, + } + return response # No matches - allow operation return {} From 7bcf6f1a0a88b6bbf4f0a6faa6624cb9de36e91c Mon Sep 17 00:00:00 2001 From: WingsOfPanda <46889518+WingsOfPanda@users.noreply.github.com> Date: Wed, 15 Apr 2026 18:50:42 +0800 Subject: [PATCH 2/2] feat(hookify): add silent flag for assistant-only warn rules MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds a `silent: true` frontmatter field to hookify rules. When set, the warn rule suppresses `systemMessage` (user-facing banner) and delivers only via `additionalContext` (assistant-facing). This enables purely Claude-internal self-correction rules without creating user notification fatigue. Behavior: - `silent: true` → no banner, only additionalContext - `silent: false` (default, backward compat) → banner + additionalContext - Mixed matching: banner shows only non-silent messages; additionalContext always contains all messages (silent + non-silent) Builds on #1420 (adds additionalContext to warn rules in the first place). Motivation: warn rules fall into two classes — those the user should see (shared decisions like "rm -rf confirm?") and those that are Claude-internal coaching ("use python3 not python"). The new flag lets rule authors distinguish them. Refs: - anthropics/claude-plugins-official#1420 (prereq — adds additionalContext) - anthropics/claude-code#15664, anthropics/claude-code#15345 (SDK PreToolUse additionalContext in v2.1.9) --- plugins/hookify/core/config_loader.py | 6 +++++- plugins/hookify/core/rule_engine.py | 19 +++++++++++++------ 2 files changed, 18 insertions(+), 7 deletions(-) diff --git a/plugins/hookify/core/config_loader.py b/plugins/hookify/core/config_loader.py index fa2fc3e36..73d1bbcfd 100644 --- a/plugins/hookify/core/config_loader.py +++ b/plugins/hookify/core/config_loader.py @@ -40,6 +40,9 @@ class Rule: action: str = "warn" # "warn" or "block" (future) tool_matcher: Optional[str] = None # Override tool matching message: str = "" # Message body from markdown + silent: bool = False # If true, suppress systemMessage (banner) and + # deliver the warning only via additionalContext + # (assistant-facing self-correction mode). @classmethod def from_dict(cls, frontmatter: Dict[str, Any], message: str) -> 'Rule': @@ -80,7 +83,8 @@ def from_dict(cls, frontmatter: Dict[str, Any], message: str) -> 'Rule': conditions=conditions, action=frontmatter.get('action', 'warn'), tool_matcher=frontmatter.get('tool_matcher'), - message=message.strip() + message=message.strip(), + silent=bool(frontmatter.get('silent', False)), ) diff --git a/plugins/hookify/core/rule_engine.py b/plugins/hookify/core/rule_engine.py index 245be82b7..e4e8c58e4 100644 --- a/plugins/hookify/core/rule_engine.py +++ b/plugins/hookify/core/rule_engine.py @@ -86,16 +86,23 @@ def evaluate_rules(self, rules: List[Rule], input_data: Dict[str, Any]) -> Dict[ # If only warnings, show them but allow operation if warning_rules: - messages = [f"**[{r.name}]**\n{r.message}" for r in warning_rules] - combined = "\n\n".join(messages) - response = {"systemMessage": combined} + all_messages = [f"**[{r.name}]**\n{r.message}" for r in warning_rules] + visible_messages = [f"**[{r.name}]**\n{r.message}" + for r in warning_rules + if not getattr(r, 'silent', False)] + response = {} + # Silent rules suppress the user-facing banner; non-silent rules + # still show systemMessage. If ALL matching rules are silent, no + # banner is emitted at all. + if visible_messages: + response["systemMessage"] = "\n\n".join(visible_messages) # PreToolUse additionalContext supported since Claude Code v2.1.9. - # PostToolUse has supported it since earlier. Inject so Claude (not - # just the user console) sees the educational message. + # PostToolUse has supported it since earlier. Inject so Claude + # (not just the user console) sees the educational message. if hook_event in ('PreToolUse', 'PostToolUse'): response["hookSpecificOutput"] = { "hookEventName": hook_event, - "additionalContext": combined, + "additionalContext": "\n\n".join(all_messages), } return response