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 51561c39e..e4e8c58e4 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 } @@ -85,10 +86,25 @@ 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) - } + 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. + if hook_event in ('PreToolUse', 'PostToolUse'): + response["hookSpecificOutput"] = { + "hookEventName": hook_event, + "additionalContext": "\n\n".join(all_messages), + } + return response # No matches - allow operation return {}