-
-
Save andrewssobral/402a553920a3db86068d3c5144e70f1e to your computer and use it in GitHub Desktop.
Claude Code hooks for working with `uv`
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| #!/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() |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| #!/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() |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| #!/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() |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| { | |
| "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" | |
| } | |
| ] | |
| } | |
| ] | |
| } | |
| } |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment