From 8d6454b74978107e6ff837232eae80dd181d274a Mon Sep 17 00:00:00 2001 From: Travis Vasceannie Date: Tue, 16 Sep 2025 21:33:47 +0000 Subject: [PATCH] x --- hooks/code_quality_guard.py | 130 ++++++++++++++++++++++++++++-------- 1 file changed, 101 insertions(+), 29 deletions(-) diff --git a/hooks/code_quality_guard.py b/hooks/code_quality_guard.py index 4545700..88ec133 100644 --- a/hooks/code_quality_guard.py +++ b/hooks/code_quality_guard.py @@ -374,7 +374,12 @@ def pretooluse_hook(hook_data: dict, config: QualityConfig) -> dict: # Only analyze for write/edit tools if tool_name not in ["Write", "Edit", "MultiEdit"]: - return {"decision": "allow"} + return { + "hookSpecificOutput": { + "hookEventName": "PreToolUse", + "permissionDecision": "allow" + } + } # Extract content based on tool type content = None @@ -390,11 +395,21 @@ def pretooluse_hook(hook_data: dict, config: QualityConfig) -> dict: # Only analyze Python files if not file_path or not file_path.endswith(".py") or not content: - return {"decision": "allow"} + return { + "hookSpecificOutput": { + "hookEventName": "PreToolUse", + "permissionDecision": "allow" + } + } # Skip analysis for configured patterns if should_skip_file(file_path, config): - return {"decision": "allow"} + return { + "hookSpecificOutput": { + "hookEventName": "PreToolUse", + "permissionDecision": "allow" + } + } try: # Store state if tracking enabled @@ -415,31 +430,59 @@ def pretooluse_hook(hook_data: dict, config: QualityConfig) -> dict: # Make decision based on enforcement mode if config.enforcement_mode == "strict": - return {"decision": "deny", "message": message} + return { + "hookSpecificOutput": { + "hookEventName": "PreToolUse", + "permissionDecision": "deny", + "permissionDecisionReason": message + } + } if config.enforcement_mode == "warn": - return {"decision": "ask", "message": message} + return { + "hookSpecificOutput": { + "hookEventName": "PreToolUse", + "permissionDecision": "ask", + "permissionDecisionReason": message + } + } # permissive return { - "decision": "allow", - "message": f"⚠️ Quality Warning:\n{message}", + "systemMessage": f"⚠️ Quality Warning:\n{message}", + "hookSpecificOutput": { + "hookEventName": "PreToolUse", + "permissionDecision": "allow" + } + } + else: + return { + "hookSpecificOutput": { + "hookEventName": "PreToolUse", + "permissionDecision": "allow" + } } - return {"decision": "allow"} # noqa: TRY300 except Exception as e: # noqa: BLE001 return { - "decision": "allow", - "message": f"Warning: Code quality check failed with error: {e}", + "systemMessage": f"Warning: Code quality check failed with error: {e}", + "hookSpecificOutput": { + "hookEventName": "PreToolUse", + "permissionDecision": "allow" + } } def posttooluse_hook(hook_data: dict, config: QualityConfig) -> dict: """Handle PostToolUse hook - verify quality after write/edit.""" tool_name = hook_data.get("tool_name", "") - tool_output = hook_data.get("tool_output", {}) + tool_output = hook_data.get("tool_response", {}) # Only process write/edit tools if tool_name not in ["Write", "Edit", "MultiEdit"]: - return {"decision": "allow"} + return { + "hookSpecificOutput": { + "hookEventName": "PostToolUse" + } + } # Extract file path from output file_path = None @@ -451,10 +494,18 @@ def posttooluse_hook(hook_data: dict, config: QualityConfig) -> dict: file_path = match[1] if not file_path or not file_path.endswith(".py"): - return {"decision": "allow"} + return { + "hookSpecificOutput": { + "hookEventName": "PostToolUse" + } + } if not Path(file_path).exists(): - return {"decision": "allow"} + return { + "hookSpecificOutput": { + "hookEventName": "PostToolUse" + } + } issues = [] @@ -479,14 +530,27 @@ def posttooluse_hook(hook_data: dict, config: QualityConfig) -> dict: f"📝 Post-write quality notes for {Path(file_path).name}:\n" + "\n".join(issues) ) - return {"decision": "allow", "message": message} - if config.show_success: return { - "decision": "allow", - "message": f"✅ {Path(file_path).name} passed post-write verification", + "systemMessage": message, + "hookSpecificOutput": { + "hookEventName": "PostToolUse", + "additionalContext": message + } + } + if config.show_success: + message = f"✅ {Path(file_path).name} passed post-write verification" + return { + "systemMessage": message, + "hookSpecificOutput": { + "hookEventName": "PostToolUse" + } } - return {"decision": "allow"} + return { + "hookSpecificOutput": { + "hookEventName": "PostToolUse" + } + } def main() -> None: @@ -499,11 +563,16 @@ def main() -> None: try: hook_data = json.load(sys.stdin) except json.JSONDecodeError: - print(json.dumps({"decision": "allow"})) # noqa: T201 + print(json.dumps({ + "hookSpecificOutput": { + "hookEventName": "PreToolUse", + "permissionDecision": "allow" + } + })) # noqa: T201 return - # Detect hook type based on tool_output (PostToolUse) vs tool_input (PreToolUse) - if "tool_output" in hook_data: + # Detect hook type based on tool_response (PostToolUse) vs tool_input (PreToolUse) + if "tool_response" in hook_data: # PostToolUse hook response = posttooluse_hook(hook_data, config) else: @@ -512,16 +581,19 @@ def main() -> None: print(json.dumps(response)) # noqa: T201 - # Handle exit codes according to Claude Code spec - if response.get("decision") == "deny": + # Handle exit codes based on hook output + hook_output = response.get("hookSpecificOutput", {}) + permission_decision = hook_output.get("permissionDecision") + + if permission_decision == "deny": # Exit code 2: Blocking error - stderr fed back to Claude - if "message" in response: - sys.stderr.write(response["message"]) + reason = hook_output.get("permissionDecisionReason", "Permission denied") + sys.stderr.write(reason) sys.exit(2) - elif response.get("decision") == "ask": + elif permission_decision == "ask": # Also use exit code 2 for ask decisions to ensure Claude sees the message - if "message" in response: - sys.stderr.write(response["message"]) + reason = hook_output.get("permissionDecisionReason", "Permission request") + sys.stderr.write(reason) sys.exit(2) # Exit code 0: Success (default)