by Glenn Matlin / glennmatlin
on all socials
- Download and copy all files in this gist to
~/.claude/
- Move the
.py
files 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" | |
} | |
] | |
} | |
] | |
} | |
} |