From 3d2166eec9b18ff18acccaedf2e04b3a3dfa6f0e Mon Sep 17 00:00:00 2001 From: Thariq Shihipar Date: Wed, 2 Jul 2025 09:32:33 -0700 Subject: [PATCH 1/2] proper hook file --- .../hooks/bash_command_validator_example.py | 84 +++++++++++++++++++ 1 file changed, 84 insertions(+) create mode 100644 examples/hooks/bash_command_validator_example.py diff --git a/examples/hooks/bash_command_validator_example.py b/examples/hooks/bash_command_validator_example.py new file mode 100644 index 0000000..daed91b --- /dev/null +++ b/examples/hooks/bash_command_validator_example.py @@ -0,0 +1,84 @@ +#!/usr/bin/env python3 +""" +Claude Code Hook: Bash Command Validator +========================================= +This hook runs as a PreToolUse hook for the Bash tool. +It validates bash commands against a set of rules before execution. +In this case it changes grep calls to using rg. + +Read more about hooks here: https://docs.anthropic.com/en/docs/claude-code/hooks + +Make sure to change your path to your actual script. + +{ + "hooks": { + "PreToolUse": [ + { + "matcher": "Bash", + "hooks": [ + { + "type": "command", + "command": "python -m ./bash_command_validator_example" + } + ] + } + ] + } +} + +""" + +import json +import re +import sys + +# Define validation rules as a list of (regex pattern, message) tuples +VALIDATION_RULES = [ + ( + r"\bgrep\b(?!.*\|)", + "Use 'rg' (ripgrep) instead of 'grep' for better performance and features", + ), + ( + r"\bfind\s+\S+\s+-name\b", + "Use 'rg --files | rg pattern' or 'rg --files -g pattern' instead of 'find -name' for better performance", + ), +] + + +def validate_command(command: str) -> list[str]: + issues = [] + for pattern, message in VALIDATION_RULES: + if re.search(pattern, command): + issues.append(message) + return issues + + +def main(): + try: + input_data = json.load(sys.stdin) + except json.JSONDecodeError as e: + print(f"Error: Invalid JSON input: {e}", file=sys.stderr) + sys.exit(1) + + tool_name = input_data.get("tool_name", "") + if tool_name != "Bash": + sys.exit(0) + + tool_input = input_data.get("tool_input", {}) + command = tool_input.get("command", "") + + if not command: + sys.exit(0) + + # Validate the command + issues = validate_command(command) + + if issues: + for message in issues: + print(f"• {message}", file=sys.stderr) + # Exit code 2 blocks tool call and shows stderr to Claude + sys.exit(2) + + +if __name__ == "__main__": + main() From 74ba615503bb3932fde2082b4eedddb819a4a3c4 Mon Sep 17 00:00:00 2001 From: Dickson Tsai Date: Wed, 2 Jul 2025 11:00:03 -0700 Subject: [PATCH 2/2] Script polish --- .../hooks/bash_command_validator_example.py | 17 ++++++++--------- 1 file changed, 8 insertions(+), 9 deletions(-) diff --git a/examples/hooks/bash_command_validator_example.py b/examples/hooks/bash_command_validator_example.py index daed91b..53ab7a8 100644 --- a/examples/hooks/bash_command_validator_example.py +++ b/examples/hooks/bash_command_validator_example.py @@ -18,7 +18,7 @@ Make sure to change your path to your actual script. "hooks": [ { "type": "command", - "command": "python -m ./bash_command_validator_example" + "command": "python3 /path/to/claude-code/examples/hooks/bash_command_validator_example.py" } ] } @@ -33,21 +33,21 @@ import re import sys # Define validation rules as a list of (regex pattern, message) tuples -VALIDATION_RULES = [ +_VALIDATION_RULES = [ ( - r"\bgrep\b(?!.*\|)", + r"^grep\b(?!.*\|)", "Use 'rg' (ripgrep) instead of 'grep' for better performance and features", ), ( - r"\bfind\s+\S+\s+-name\b", + r"^find\s+\S+\s+-name\b", "Use 'rg --files | rg pattern' or 'rg --files -g pattern' instead of 'find -name' for better performance", ), ] -def validate_command(command: str) -> list[str]: +def _validate_command(command: str) -> list[str]: issues = [] - for pattern, message in VALIDATION_RULES: + for pattern, message in _VALIDATION_RULES: if re.search(pattern, command): issues.append(message) return issues @@ -58,6 +58,7 @@ def main(): input_data = json.load(sys.stdin) except json.JSONDecodeError as e: print(f"Error: Invalid JSON input: {e}", file=sys.stderr) + # Exit code 1 shows stderr to the user but not to Claude sys.exit(1) tool_name = input_data.get("tool_name", "") @@ -70,9 +71,7 @@ def main(): if not command: sys.exit(0) - # Validate the command - issues = validate_command(command) - + issues = _validate_command(command) if issues: for message in issues: print(f"• {message}", file=sys.stderr)