| 1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162 |
- #!/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
|