by Glenn Matlin / glennmatlin on all socials
- Download and copy all files in this gist to
~/.claude/ - Move the
.pyfiles to~/.claude/hooks - Restart Claude Code.
| #!/usr/bin/env python3 | |
| # /// script | |
| # requires-python = ">=3.8" | |
| # dependencies = [] | |
| # /// | |
| """ | |
| Notification hook for UV-related reminders. | |
| """ | |
| import json | |
| import sys | |
| import re | |
| def main(): | |
| """Main hook function.""" | |
| try: | |
| # Read input | |
| input_data = json.loads(sys.stdin.read()) | |
| message = input_data.get('message', '') | |
| # Check for Python-related permission requests | |
| python_keywords = ['python', 'pip', 'install', 'package', 'dependency'] | |
| if any(keyword in message.lower() for keyword in python_keywords): | |
| reminder = "\\n💡 Reminder: Use UV commands (uv run, uv add) instead of pip/python directly." | |
| # Provide context-specific suggestions | |
| if 'install' in message.lower(): | |
| reminder += "\\n To add packages: uv add <package_name>" | |
| if 'python' in message.lower() and 'run' in message.lower(): | |
| reminder += "\\n To run Python: uv run python or uv run script.py" | |
| print(reminder, file=sys.stderr) | |
| sys.exit(0) | |
| except Exception as e: | |
| print(f"Notification hook error: {str(e)}", file=sys.stderr) | |
| sys.exit(1) | |
| if __name__ == "__main__": | |
| main() |
| #!/usr/bin/env python3 | |
| # /// script | |
| # requires-python = ">=3.8" | |
| # dependencies = [] | |
| # /// | |
| """ | |
| Pre-tool use hook for Claude Code to guide UV usage in Python projects. | |
| Since PreToolUse hooks cannot modify commands (Claude Code limitation), | |
| this hook provides helpful guidance when Python commands are used in UV projects. | |
| """ | |
| import json | |
| import sys | |
| import re | |
| import os | |
| from pathlib import Path | |
| from typing import Dict, Any | |
| class UVCommandHandler: | |
| """Handle Python commands with UV awareness.""" | |
| def __init__(self): | |
| self.project_root = Path.cwd() | |
| self.has_uv = self.check_uv_available() | |
| self.in_project = self.check_in_project() | |
| def check_uv_available(self) -> bool: | |
| """Check if UV is available in PATH.""" | |
| return os.system("which uv > /dev/null 2>&1") == 0 | |
| def check_in_project(self) -> bool: | |
| """Check if we're in a Python project with pyproject.toml.""" | |
| return (self.project_root / "pyproject.toml").exists() | |
| def analyze_command(self, command: str) -> Dict[str, Any]: | |
| """Analyze command to determine how to handle it.""" | |
| # Check if command already uses uv | |
| if command.strip().startswith('uv'): | |
| return {"action": "approve", "reason": "Already using uv"} | |
| # Skip non-Python commands entirely | |
| # Common commands that should never be blocked | |
| skip_prefixes = ['git ', 'cd ', 'ls ', 'cat ', 'echo ', 'grep ', 'find ', 'mkdir ', 'rm ', 'cp ', 'mv '] | |
| if any(command.strip().startswith(prefix) for prefix in skip_prefixes): | |
| return {"action": "approve", "reason": "Not a Python command"} | |
| # Check for actual Python command execution (not just mentions) | |
| python_exec_patterns = [ | |
| r'^python3?\s+', # python script.py | |
| r'^python3?\s*$', # just python | |
| r'\|\s*python3?\s+', # piped to python | |
| r';\s*python3?\s+', # after semicolon | |
| r'&&\s*python3?\s+', # after && | |
| r'^pip3?\s+', # pip commands | |
| r'\|\s*pip3?\s+', # piped pip | |
| r';\s*pip3?\s+', # after semicolon | |
| r'&&\s*pip3?\s+', # after && | |
| r'^(pytest|ruff|mypy|black|flake8|isort)\s+', # dev tools | |
| r';\s*(pytest|ruff|mypy|black|flake8|isort)\s+', | |
| r'&&\s*(pytest|ruff|mypy|black|flake8|isort)\s+', | |
| ] | |
| is_python_exec = any(re.search(pattern, command) for pattern in python_exec_patterns) | |
| if not is_python_exec: | |
| return {"action": "approve", "reason": "Not a Python execution command"} | |
| # If we're in a UV project, provide guidance | |
| if self.has_uv and self.in_project: | |
| # Parse command to provide better suggestions | |
| suggestion = self.suggest_uv_command(command) | |
| return { | |
| "action": "block", | |
| "reason": f"This project uses UV for Python management. {suggestion}" | |
| } | |
| # Otherwise, let it through | |
| return {"action": "approve", "reason": "UV not required"} | |
| def suggest_uv_command(self, command: str) -> str: | |
| """Provide UV command suggestions.""" | |
| # Handle compound commands (e.g., cd && python) | |
| if '&&' in command: | |
| parts = command.split('&&') | |
| transformed_parts = [] | |
| for part in parts: | |
| part = part.strip() | |
| # Only transform the Python-related parts | |
| if re.search(r'\b(python3?|pip3?|pytest|ruff|mypy|black)\b', part): | |
| transformed_parts.append(self._transform_single_command(part)) | |
| else: | |
| transformed_parts.append(part) | |
| return f"Try: {' && '.join(transformed_parts)}" | |
| # Simple commands | |
| return f"Try: {self._transform_single_command(command)}" | |
| def _transform_single_command(self, command: str) -> str: | |
| """Transform a single Python command to use UV.""" | |
| # Python execution | |
| if re.match(r'^python3?\s+', command): | |
| return re.sub(r'^python3?\s+', 'uv run python ', command) | |
| # Package installation | |
| elif re.match(r'^pip3?\s+install\s+', command): | |
| return re.sub(r'^pip3?\s+install\s+', 'uv add ', command) | |
| # Other pip commands | |
| elif re.match(r'^pip3?\s+', command): | |
| return re.sub(r'^pip3?\s+', 'uv pip ', command) | |
| # Development tools | |
| elif re.match(r'^(pytest|ruff|mypy|black|flake8|isort)\s+', command): | |
| return f'uv run {command}' | |
| return command | |
| def main(): | |
| """Main hook function.""" | |
| try: | |
| # Read input from Claude Code | |
| input_data = json.loads(sys.stdin.read()) | |
| # Only process Bash/Run commands | |
| tool_name = input_data.get('tool_name', '') | |
| if tool_name not in ['Bash', 'Run']: | |
| # Approve non-Bash tools | |
| output = {"decision": "approve"} | |
| print(json.dumps(output)) | |
| return | |
| # Get the command | |
| tool_input = input_data.get('tool_input', {}) | |
| command = tool_input.get('command', '') | |
| if not command: | |
| # Approve empty commands | |
| output = {"decision": "approve"} | |
| print(json.dumps(output)) | |
| return | |
| # Analyze command | |
| handler = UVCommandHandler() | |
| result = handler.analyze_command(command) | |
| # Return decision | |
| output = { | |
| "decision": result["action"], | |
| "reason": result["reason"] | |
| } | |
| print(json.dumps(output)) | |
| except Exception as e: | |
| # On error, approve to avoid blocking workflow | |
| output = { | |
| "decision": "approve", | |
| "reason": f"Hook error: {str(e)}" | |
| } | |
| print(json.dumps(output)) | |
| if __name__ == "__main__": | |
| main() |
| #!/usr/bin/env python3 | |
| # /// script | |
| # requires-python = ">=3.8" | |
| # dependencies = [] | |
| # /// | |
| """ | |
| Pre-tool use hook for Claude Code to guide UV usage in Python projects. | |
| Since PreToolUse hooks cannot modify commands (Claude Code limitation), | |
| this hook provides helpful guidance when Python commands are used in UV projects. | |
| """ | |
| import json | |
| import sys | |
| import re | |
| import os | |
| from pathlib import Path | |
| from typing import Dict, Any | |
| class UVCommandHandler: | |
| """Handle Python commands with UV awareness.""" | |
| def __init__(self): | |
| self.project_root = Path.cwd() | |
| self.has_uv = self.check_uv_available() | |
| self.in_project = self.check_in_project() | |
| def check_uv_available(self) -> bool: | |
| """Check if UV is available in PATH.""" | |
| return os.system("which uv > /dev/null 2>&1") == 0 | |
| def check_in_project(self) -> bool: | |
| """Check if we're in a Python project with pyproject.toml.""" | |
| return (self.project_root / "pyproject.toml").exists() | |
| def analyze_command(self, command: str) -> Dict[str, Any]: | |
| """Analyze command to determine how to handle it.""" | |
| # Check if command already uses uv | |
| if command.strip().startswith('uv'): | |
| return {"action": "approve", "reason": "Already using uv"} | |
| # Skip non-Python commands entirely | |
| # Common commands that should never be blocked | |
| skip_prefixes = ['git ', 'cd ', 'ls ', 'cat ', 'echo ', 'grep ', 'find ', 'mkdir ', 'rm ', 'cp ', 'mv '] | |
| if any(command.strip().startswith(prefix) for prefix in skip_prefixes): | |
| return {"action": "approve", "reason": "Not a Python command"} | |
| # Check for actual Python command execution (not just mentions) | |
| python_exec_patterns = [ | |
| r'^python3?\s+', # python script.py | |
| r'^python3?\s*$', # just python | |
| r'\|\s*python3?\s+', # piped to python | |
| r';\s*python3?\s+', # after semicolon | |
| r'&&\s*python3?\s+', # after && | |
| r'^pip3?\s+', # pip commands | |
| r'\|\s*pip3?\s+', # piped pip | |
| r';\s*pip3?\s+', # after semicolon | |
| r'&&\s*pip3?\s+', # after && | |
| r'^(pytest|ruff|mypy|black|flake8|isort)\s+', # dev tools | |
| r';\s*(pytest|ruff|mypy|black|flake8|isort)\s+', | |
| r'&&\s*(pytest|ruff|mypy|black|flake8|isort)\s+', | |
| ] | |
| is_python_exec = any(re.search(pattern, command) for pattern in python_exec_patterns) | |
| if not is_python_exec: | |
| return {"action": "approve", "reason": "Not a Python execution command"} | |
| # If we're in a UV project, provide guidance | |
| if self.has_uv and self.in_project: | |
| # Parse command to provide better suggestions | |
| suggestion = self.suggest_uv_command(command) | |
| return { | |
| "action": "block", | |
| "reason": f"This project uses UV for Python management. {suggestion}" | |
| } | |
| # Otherwise, let it through | |
| return {"action": "approve", "reason": "UV not required"} | |
| def suggest_uv_command(self, command: str) -> str: | |
| """Provide UV command suggestions.""" | |
| # Handle compound commands (e.g., cd && python) | |
| if '&&' in command: | |
| parts = command.split('&&') | |
| transformed_parts = [] | |
| for part in parts: | |
| part = part.strip() | |
| # Only transform the Python-related parts | |
| if re.search(r'\b(python3?|pip3?|pytest|ruff|mypy|black)\b', part): | |
| transformed_parts.append(self._transform_single_command(part)) | |
| else: | |
| transformed_parts.append(part) | |
| return f"Try: {' && '.join(transformed_parts)}" | |
| # Simple commands | |
| return f"Try: {self._transform_single_command(command)}" | |
| def _transform_single_command(self, command: str) -> str: | |
| """Transform a single Python command to use UV.""" | |
| # Python execution | |
| if re.match(r'^python3?\s+', command): | |
| return re.sub(r'^python3?\s+', 'uv run python ', command) | |
| # Package installation | |
| elif re.match(r'^pip3?\s+install\s+', command): | |
| return re.sub(r'^pip3?\s+install\s+', 'uv add ', command) | |
| # Other pip commands | |
| elif re.match(r'^pip3?\s+', command): | |
| return re.sub(r'^pip3?\s+', 'uv pip ', command) | |
| # Development tools | |
| elif re.match(r'^(pytest|ruff|mypy|black|flake8|isort)\s+', command): | |
| return f'uv run {command}' | |
| return command | |
| def main(): | |
| """Main hook function.""" | |
| try: | |
| # Read input from Claude Code | |
| input_data = json.loads(sys.stdin.read()) | |
| # Only process Bash/Run commands | |
| tool_name = input_data.get('tool_name', '') | |
| if tool_name not in ['Bash', 'Run']: | |
| # Approve non-Bash tools | |
| output = {"decision": "approve"} | |
| print(json.dumps(output)) | |
| return | |
| # Get the command | |
| tool_input = input_data.get('tool_input', {}) | |
| command = tool_input.get('command', '') | |
| if not command: | |
| # Approve empty commands | |
| output = {"decision": "approve"} | |
| print(json.dumps(output)) | |
| return | |
| # Analyze command | |
| handler = UVCommandHandler() | |
| result = handler.analyze_command(command) | |
| # Return decision | |
| output = { | |
| "decision": result["action"], | |
| "reason": result["reason"] | |
| } | |
| print(json.dumps(output)) | |
| except Exception as e: | |
| # On error, approve to avoid blocking workflow | |
| output = { | |
| "decision": "approve", | |
| "reason": f"Hook error: {str(e)}" | |
| } | |
| print(json.dumps(output)) | |
| if __name__ == "__main__": | |
| main() |
| { | |
| "includeCoAuthoredBy": false, | |
| "hooks": { | |
| "PreToolUse": [ | |
| { | |
| "matcher": "Bash|Run", | |
| "hooks": [ | |
| { | |
| "type": "command", | |
| "command": "python3 ~/.claude/hooks/pre_tool_use_uv.py", | |
| "timeout": 10 | |
| } | |
| ] | |
| } | |
| ], | |
| "PostToolUse": [ | |
| { | |
| "matcher": "Bash|Run", | |
| "hooks": [ | |
| { | |
| "type": "command", | |
| "command": "python3 ~/.claude/hooks/post_tool_use_uv.py" | |
| } | |
| ] | |
| } | |
| ], | |
| "Notification": [ | |
| { | |
| "matcher": "", | |
| "hooks": [ | |
| { | |
| "type": "command", | |
| "command": "python3 ~/.claude/hooks/notification_uv.py" | |
| } | |
| ] | |
| } | |
| ] | |
| } | |
| } |