|
|
@@ -0,0 +1,62 @@
|
|
|
+#!/bin/bash
|
|
|
+# hooks/enforce-uv.sh
|
|
|
+# PreToolUse hook - enforces uv over pip / bare tools inside uv-managed projects
|
|
|
+# Matcher: Bash
|
|
|
+#
|
|
|
+# Turns the "modern-tools" guidance (a should-do prompt) into a deterministic
|
|
|
+# must-do guard. Redirects:
|
|
|
+# pip install <pkg> -> uv add <pkg> (or `uv pip ...` for unmanaged envs)
|
|
|
+# pytest / ruff / mypy ... -> uv run <tool>
|
|
|
+#
|
|
|
+# Configuration in .claude/settings.json:
|
|
|
+# {
|
|
|
+# "hooks": {
|
|
|
+# "PreToolUse": [{
|
|
|
+# "matcher": "Bash",
|
|
|
+# "hooks": ["bash hooks/enforce-uv.sh $TOOL_INPUT"]
|
|
|
+# }]
|
|
|
+# }
|
|
|
+# }
|
|
|
+#
|
|
|
+# Exit codes:
|
|
|
+# 0 = allow (not a Python project, already uv, or no violation)
|
|
|
+# 2 = block with guidance
|
|
|
+#
|
|
|
+# Scope guards:
|
|
|
+# - Only activates when a pyproject.toml exists in the working directory
|
|
|
+# (i.e. a uv-managed project). Outside one, pip/bare tools pass through.
|
|
|
+# - Honors ENFORCE_UV=0 to disable for a single command or session.
|
|
|
+
|
|
|
+INPUT="$1"
|
|
|
+
|
|
|
+[[ -z "$INPUT" ]] && exit 0
|
|
|
+[[ "$ENFORCE_UV" == "0" ]] && exit 0
|
|
|
+
|
|
|
+# Only enforce inside a uv-managed project
|
|
|
+[[ -f "pyproject.toml" ]] || exit 0
|
|
|
+
|
|
|
+block() {
|
|
|
+ echo "BLOCKED (enforce-uv): $1"
|
|
|
+ echo "Use instead: $2"
|
|
|
+ echo ""
|
|
|
+ echo "This project has a pyproject.toml — prefer the uv workflow."
|
|
|
+ echo "To bypass for one command, prefix it with ENFORCE_UV=0."
|
|
|
+ exit 2
|
|
|
+}
|
|
|
+
|
|
|
+# --- pip install (mask the allowed `uv pip` compatibility layer first) -------
|
|
|
+MASKED=$(printf '%s' "$INPUT" | sed -E 's/\buv pip\b/UV_PIP/g')
|
|
|
+if printf '%s' "$MASKED" | grep -qE '\bpip[0-9.]*[[:space:]]+install\b'; then
|
|
|
+ block "bare 'pip install'" "uv add <pkg> (or 'uv pip install ...' for an unmanaged venv)"
|
|
|
+fi
|
|
|
+
|
|
|
+# --- bare dev tools that should run inside the project env -------------------
|
|
|
+# Skip if the command already routes through uv (uv run / uvx).
|
|
|
+if ! printf '%s' "$INPUT" | grep -qE '\b(uv run|uvx)\b'; then
|
|
|
+ if printf '%s' "$INPUT" | grep -qE '(^|[;&|][[:space:]]*)(pytest|ruff|mypy|pyright|black|isort|flake8)\b'; then
|
|
|
+ TOOL=$(printf '%s' "$INPUT" | grep -oE '(pytest|ruff|mypy|pyright|black|isort|flake8)' | head -1)
|
|
|
+ block "bare '$TOOL' in a uv project" "uv run $TOOL ..."
|
|
|
+ fi
|
|
|
+fi
|
|
|
+
|
|
|
+exit 0
|