|
#!/usr/bin/env python3 |
|
""" |
|
Python Threading Blocker Hook |
|
Blocks usage of Python threading and suggests async/await patterns |
|
""" |
|
|
|
import json |
|
import os |
|
import sys |
|
import re |
|
from datetime import datetime |
|
|
|
def check_for_threading(content): |
|
"""Check if content contains threading patterns.""" |
|
if not content: |
|
return None |
|
|
|
# Patterns to detect threading usage |
|
threading_patterns = [ |
|
(r'import\s+threading', 'import threading'), |
|
(r'from\s+threading\s+import', 'from threading import'), |
|
(r'threading\.Thread', 'threading.Thread'), |
|
(r'Thread\s*\(', 'Thread class instantiation'), |
|
(r'\.start\s*\(\s*\)', 'thread.start()'), |
|
(r'\.join\s*\(\s*\)', 'thread.join()'), |
|
(r'threading\.Lock', 'threading.Lock'), |
|
(r'threading\.Event', 'threading.Event'), |
|
(r'threading\.Semaphore', 'threading.Semaphore'), |
|
(r'concurrent\.futures\.ThreadPoolExecutor', 'ThreadPoolExecutor'), |
|
] |
|
|
|
detected_patterns = [] |
|
|
|
for pattern, description in threading_patterns: |
|
if re.search(pattern, content, re.IGNORECASE | re.MULTILINE): |
|
detected_patterns.append(description) |
|
|
|
return detected_patterns if detected_patterns else None |
|
|
|
def generate_async_suggestion(detected_patterns): |
|
"""Generate suggestions for async/await alternatives.""" |
|
suggestions = { |
|
'import threading': 'Use: import asyncio', |
|
'from threading import': 'Use: import asyncio', |
|
'threading.Thread': 'Use: asyncio.create_task() or async def functions', |
|
'Thread class instantiation': 'Use: async def function and asyncio.create_task()', |
|
'thread.start()': 'Use: await asyncio.create_task(your_async_function())', |
|
'thread.join()': 'Use: await task or asyncio.gather(*tasks)', |
|
'threading.Lock': 'Use: asyncio.Lock()', |
|
'threading.Event': 'Use: asyncio.Event()', |
|
'threading.Semaphore': 'Use: asyncio.Semaphore()', |
|
'ThreadPoolExecutor': 'Use: asyncio.create_task() for I/O bound, or asyncio.run_in_executor() for CPU-bound tasks' |
|
} |
|
|
|
advice = [] |
|
for pattern in detected_patterns: |
|
if pattern in suggestions: |
|
advice.append(f"โข {pattern} โ {suggestions[pattern]}") |
|
else: |
|
advice.append(f"โข {pattern} โ Consider async/await pattern") |
|
|
|
return advice |
|
|
|
def main(): |
|
"""Main hook function.""" |
|
try: |
|
# Hook data comes via stdin as JSON |
|
hook_input = sys.stdin.read().strip() |
|
|
|
debug_log = "/tmp/threading_hook_debug.log" |
|
with open(debug_log, "a") as f: |
|
f.write(f"\n=== Threading Hook Debug {datetime.now().isoformat()} ===\n") |
|
f.write(f"Hook input from stdin: {hook_input}\n") |
|
|
|
if not hook_input: |
|
sys.exit(0) |
|
|
|
hook_data = json.loads(hook_input) |
|
|
|
with open(debug_log, "a") as f: |
|
f.write(f"Parsed hook_data keys: {list(hook_data.keys())}\n") |
|
f.write(f"Hook data: {json.dumps(hook_data, indent=2)}\n") |
|
|
|
tool_name = hook_data.get('tool_name', '') or os.environ.get('CLAUDE_TOOL_NAME', '') |
|
tool_input = hook_data.get('tool_input', {}) or hook_data |
|
|
|
with open(debug_log, "a") as f: |
|
f.write(f"Tool name: {tool_name}\n") |
|
f.write(f"Tool input keys: {list(tool_input.keys())}\n") |
|
|
|
# Only check Write, Edit, MultiEdit tools for Python content |
|
if tool_name not in ['Write', 'Edit', 'MultiEdit']: |
|
with open(debug_log, "a") as f: |
|
f.write(f"Skipping tool: {tool_name}\n") |
|
sys.exit(0) |
|
|
|
# Check if it's a Python file |
|
file_path = tool_input.get('file_path', '') |
|
if not file_path.endswith('.py'): |
|
with open(debug_log, "a") as f: |
|
f.write(f"Skipping non-Python file: {file_path}\n") |
|
sys.exit(0) |
|
|
|
with open(debug_log, "a") as f: |
|
f.write(f"Checking Python file: {file_path}\n") |
|
|
|
# Get content to check |
|
content = None |
|
if tool_name == 'Write': |
|
content = tool_input.get('content', '') |
|
elif tool_name == 'Edit': |
|
content = tool_input.get('new_string', '') |
|
elif tool_name == 'MultiEdit': |
|
# Check all edits |
|
edits = tool_input.get('edits', []) |
|
all_content = [] |
|
for edit in edits: |
|
all_content.append(edit.get('new_string', '')) |
|
content = '\n'.join(all_content) |
|
|
|
# Check for threading patterns |
|
detected = check_for_threading(content) |
|
if detected: |
|
# Generate helpful error message |
|
suggestions = generate_async_suggestion(detected) |
|
|
|
error_msg = f"""๐ซ Threading usage detected in {file_path} |
|
|
|
Detected patterns: |
|
{chr(10).join(f"โข {pattern}" for pattern in detected)} |
|
|
|
๐ Recommended async/await alternatives: |
|
{chr(10).join(suggestions)} |
|
|
|
๐ก Why async/await? |
|
โข Better performance for I/O-bound operations |
|
โข Easier debugging and testing |
|
โข More predictable execution model |
|
โข Better integration with modern Python frameworks |
|
|
|
Example conversion: |
|
```python |
|
# Instead of: |
|
import threading |
|
def worker(): |
|
# do work |
|
pass |
|
thread = threading.Thread(target=worker) |
|
thread.start() |
|
thread.join() |
|
|
|
# Use: |
|
import asyncio |
|
async def worker(): |
|
# do async work |
|
pass |
|
await worker() |
|
``` |
|
|
|
Please refactor to use async/await patterns instead of threading.""" |
|
|
|
print(error_msg, file=sys.stderr) |
|
sys.exit(2) # Block the operation |
|
|
|
# No threading detected, allow operation |
|
sys.exit(0) |
|
|
|
except Exception as e: |
|
# Don't block on errors, just log |
|
print(f"Threading check failed: {e}", file=sys.stderr) |
|
sys.exit(0) |
|
|
|
if __name__ == '__main__': |
|
main() |
Code Review for Claude Code Python Hooks
Hi Jeremy,
Thank you for sharing your collection of Python hooks for Claude Code. This is an excellent initiative! These "Power Pack" scripts demonstrate a deep understanding of practical developer needs and showcase the power of the hooks system for enforcing compliance, maintaining code quality, and improving security. The overall quality is high, and the goals of each script are clear and valuable.
This review provides feedback on the implementation, cross-referencing it with the official Claude Code documentation you provided to ensure the scripts are robust, portable, and future-proof.
High-Level Feedback & Key Recommendations
The scripts are well-written, but there's a critical discrepancy in how they receive data from Claude Code compared to the documented API. Addressing this will be key to ensuring they work reliably for all users.
Data Input Mechanism (Critical): The
format_code.pyscript and parts ofblock_threading.pyrely on environment variables (CLAUDE_TOOL_ARGS,CLAUDE_TOOL_NAME) to get information about the tool call. The officialhooksandhooks-guidedocumentation is very clear that hook data is passed as a JSON object viastdin.hooksdocumentation states: "Hooks receive JSON data via stdin containing session information and event-specific data." It then provides detailed schemas forPreToolUseandPostToolUseevents.sys.stdinand parse the JSON to get tool information. Relying on undocumented environment variables is risky, as they could be changed or removed in future versions of Claude Code. Theaudit_logger.pyandblock_secrets.pyscripts already do this perfectly, and they should be used as the model for the others.Installation Portability: The
install.mdguide uses a hardcoded absolute path (/Users/your_username/...). This is not portable across different users or operating systems.~) character (e.g.,~/.claude-hooks/audit_logger.py), which is expanded by most shells to the user's home directory. This makes the configuration snippet copy-paste friendly for everyone.Hook Configuration Efficiency: The
install.mdguide registers each hook as a separate command insettings.json. For a "pack" of related scripts, it can be more efficient to have a single "router" script that takes an argument and executes the appropriate logic. This is a minor suggestion for a potential future enhancement, as the current method is perfectly valid.Script-by-Script Review
1.
audit_logger.py(PostToolUse)This script is excellent and serves as a fantastic example of how to write a robust, compliance-aware hook.
What's Great:
stdinand parses it, perfectly aligning with the official documentation.content_size) instead of full, potentially sensitive content is a brilliant design choice. This makes the audit log safe and useful without creating a security liability.pathlibfor path manipulation and writes logs to a sensible location (~/.claude/).Suggestions:
2.
block_secrets.py(PreToolUse)This is a simple, effective, and critical security hook. It's perfectly implemented.
What's Great:
PreToolUseevent to intercept the action before it happens.sys.exit(2)and prints a detailed, helpful error message tostderr.hooksdocumentation: "Exit code 2: Blocking error.stderris fed back to Claude to process automatically." The error message is well-crafted to help Claude understand the policy and correct its behavior.Suggestions:
sensitive_extensionslist to include common variations like.secret,.credentials, or files likeid_rsa.3.
block_threading.py(PreToolUse)This is a fantastic example of using hooks to enforce opinionated coding standards. The feedback provided to the model is top-notch.
What's Great:
stderris a model of what a good blocking hook should do. It explains why the action was blocked, lists the detected patterns, suggests a specific alternative (asyncio), and even provides a code example. This is extremely effective for guiding the LLM.Suggestions:
stdin, but it also includes fallback logic to check environment variables (os.environ.get('CLAUDE_TOOL_NAME', '')). This fallback should be removed in favor of relying solely on the documentedstdinAPI for consistency and reliability./tmp/threading_hook_debug.log). For a distributable script, it would be better to make this opt-in, perhaps by checking for an environment variable likeCLAUDE_HOOK_DEBUG=1.4.
format_code.py(PostToolUse)The concept is excellent and highly useful, but the implementation needs to be aligned with the official hooks API.
What's Great:
PostToolUsehook.ruff,rustfmt,gofmt,prettier).Critical Issue for Correction:
os.environ.get('CLAUDE_TOOL_ARGS', '{}')to get the file path. This will fail in a standard Claude Code environment that passes data viastdin.PostToolUse Inputschema in thehooksdocumentation shows that thetool_inputobject, containing thefile_path, is part of the JSON payload sent tostdin.sys.stdin, parse the JSON, and extract thefile_pathfrom thetool_inputkey.Example Fix:
install.mdReviewThe installation guide is clear and easy to follow. A few small tweaks will make it even better.
Suggestions:
~for Home Directory: In thesettings.jsonexample, replace/Users/your_username/with~/. This will work for almost all users on macOS and Linux out-of-the-box.if echo \"$CLAUDE_TOOL_ARGS\"...relies on the same undocumented environment variable asformat_code.py. This should be replaced with a proper hook script that reads fromstdin. A simplified version ofblock_secrets.pycould achieve this. For example,~/.claude-hooks/block_env_files.py.PATH. Thechmodcommand won't work, but it's also not necessary. Adding a small "For Windows Users" note could be helpful.Final Thoughts
This is a very impressive and highly practical set of tools. The
audit_loggerandblock_secretsscripts are perfect as-is, and theblock_threadingscript is nearly perfect. With the recommended change to the data input mechanism informat_code.py, this "Power Pack" will be a robust, reliable, and incredibly useful addition to any developer's Claude Code setup.It's clear you've put a lot of thought into creating genuinely useful automations. Keep up the fantastic work