-
-
Save mvandermeulen/ff1b653e4f762f2d206cc4d1dfa82895 to your computer and use it in GitHub Desktop.
Revisions
-
glennmatlin renamed this gist
Jul 26, 2025 . 1 changed file with 0 additions and 0 deletions.There are no files selected for viewing
File renamed without changes. -
glennmatlin revised this gist
Jul 26, 2025 . 3 changed files with 7 additions and 0 deletions.There are no files selected for viewing
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 charactersOriginal file line number Diff line number Diff line change @@ -0,0 +1,7 @@ # Claude Code Hooks for working with `uv` by Glenn Matlin / `glennmatlin` on all socials ## Installation 1. Download and copy all files in this gist to `~/.claude/` 2. Move the `.py` files to `~/.claude/hooks` 3. Restart Claude Code. File renamed without changes.File renamed without changes. -
glennmatlin renamed this gist
Jul 26, 2025 . 1 changed file with 0 additions and 0 deletions.There are no files selected for viewing
File renamed without changes. -
glennmatlin created this gist
Jul 26, 2025 .There are no files selected for viewing
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 charactersOriginal file line number Diff line number Diff line change @@ -0,0 +1,41 @@ #!/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 charactersOriginal file line number Diff line number Diff line change @@ -0,0 +1,166 @@ #!/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 charactersOriginal file line number Diff line number Diff line change @@ -0,0 +1,166 @@ #!/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 charactersOriginal file line number Diff line number Diff line change @@ -0,0 +1,39 @@ { "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" } ] } ] } }