claude-code/examples/hooks/bash_command_validator_example.py
2025-07-02 11:00:03 -07:00

83 lines
2 KiB
Python

#!/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": "python3 /path/to/claude-code/examples/hooks/bash_command_validator_example.py"
}
]
}
]
}
}
"""
import json
import re
import sys
# Define validation rules as a list of (regex pattern, message) tuples
_VALIDATION_RULES = [
(
r"^grep\b(?!.*\|)",
"Use 'rg' (ripgrep) instead of 'grep' for better performance and features",
),
(
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]:
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)
# Exit code 1 shows stderr to the user but not to Claude
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)
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()