Last active
September 30, 2025 22:19
-
-
Save lolpack/8aac9f88b1d02eb776a5b96f019339ce to your computer and use it in GitHub Desktop.
Claude Hooks for Pyrefly
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 | |
| """ | |
| Claude Code hook: run 'pyrefly check' to keep the repo type-clean. | |
| Behavior | |
| - PostToolUse (after Edit/Write/MultiEdit): fast check of the changed python file if possible, | |
| else fall back to full project check. | |
| - Stop: full project check at iteration end. If errors exist, block stop (exit 2) so Claude | |
| continues and fixes them. Use stop_hook_active guard to prevent loops. | |
| Exit codes per Claude Code hooks: | |
| 0 = success, 2 = blocking error (stderr is given to Claude). Others = non-blocking. | |
| """ | |
| import json | |
| import os | |
| import shutil | |
| import subprocess | |
| import sys | |
| PYTHON_SUFFIXES = (".py", ".pyi") | |
| def _is_python_file(path: str) -> bool: | |
| return path.endswith(PYTHON_SUFFIXES) | |
| def _resolve_pyrefly(project_dir: str) -> str: | |
| # Prefer venv-local pyrefly, else PATH | |
| candidates = [ | |
| os.path.join(project_dir, ".venv", "bin", "pyrefly"), | |
| os.path.join(project_dir, "venv", "bin", "pyrefly"), | |
| "pyrefly", | |
| ] | |
| for c in candidates: | |
| exe = shutil.which(c) if os.path.basename(c) == c else (c if os.path.isfile(c) and os.access(c, os.X_OK) else None) | |
| if exe: | |
| return exe | |
| return "pyrefly" # best-effort; subprocess will fail clearly | |
| def _run(cmd, cwd): | |
| return subprocess.run(cmd, cwd=cwd, text=True, capture_output=True) | |
| def main(): | |
| try: | |
| data = json.load(sys.stdin) | |
| except Exception as e: | |
| print(f"Failed to parse hook input JSON: {e}", file=sys.stderr) | |
| sys.exit(1) | |
| event = data.get("hook_event_name", "") | |
| stop_active = bool(data.get("stop_hook_active", False)) | |
| tool_input = data.get("tool_input") or {} | |
| changed_path = tool_input.get("file_path") or "" | |
| # Prefer Claude's project dir if set; fall back to hook cwd. | |
| project_dir = os.environ.get("CLAUDE_PROJECT_DIR") or data.get("cwd") or os.getcwd() | |
| pyrefly = _resolve_pyrefly(project_dir) | |
| # Avoid infinite retry loops if this Stop hook already triggered a continuation | |
| if event in ("Stop", "SubagentStop") and stop_active: | |
| print("Skipping pyrefly check to avoid loop (stop_hook_active=true).", file=sys.stderr) | |
| sys.exit(1) | |
| # Decide scope | |
| args = [] | |
| if event in ("PostToolUse", "PreToolUse") and changed_path and _is_python_file(changed_path): | |
| # Try a quick single-file check; Pyrefly still consults project config for env/imports. | |
| args = [changed_path] | |
| # Run the check | |
| result = _run([pyrefly, "check", *args], cwd=project_dir) | |
| if result.returncode == 0: | |
| # Progress text appears in transcript mode for PostToolUse/Stop. | |
| print("✓ Pyrefly type check passed") | |
| sys.exit(0) | |
| # Forward errors to Claude via stderr and block with exit code 2 | |
| # Prefer stderr; if empty, fall back to stdout. | |
| message = (result.stderr or result.stdout or "").strip() or "pyrefly check failed with unknown error." | |
| print(message, file=sys.stderr) | |
| sys.exit(2) | |
| if __name__ == "__main__": | |
| main() |
Author
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Install pyrefly with pip/uv
Create
.claude/hooks/pyrefly_check.pyand make it executable:Add to your project settings at
.claude/settings.json: