Browse Source

feat(hooks): add enforce-uv PreToolUse hook

Turns the modern-tools guidance into a deterministic guard. Inside a
uv-managed project (pyproject.toml present) it blocks:

  pip install <pkg>         -> suggests uv add <pkg>
  bare pytest/ruff/mypy/... -> suggests uv run <tool>

Masks the allowed `uv pip` compatibility layer, skips when the command
already routes through `uv run`/`uvx`, no-ops outside a project, and honors
ENFORCE_UV=0 to bypass. Verified with 13 allow/block cases (incl. no false
positive when a tool name only appears inside a commit message).

Registered in plugin.json (4 -> 5 hooks), hooks/README.md, and README.md;
also added the previously-missing check-mail.sh row to both tables.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
0xDarkMatter 1 month ago
parent
commit
304934d5fa
4 changed files with 71 additions and 4 deletions
  1. 3 2
      .claude-plugin/plugin.json
  2. 3 2
      README.md
  3. 3 0
      hooks/README.md
  4. 62 0
      hooks/enforce-uv.sh

+ 3 - 2
.claude-plugin/plugin.json

@@ -1,7 +1,7 @@
 {
   "name": "claude-mods",
   "version": "2.8.0",
-  "description": "Custom commands, skills, and agents for Claude Code - session continuity, 23 expert agents, 78 skills, 2 commands, 6 rules, 4 hooks, 13 output styles, modern CLI tools",
+  "description": "Custom commands, skills, and agents for Claude Code - session continuity, 23 expert agents, 78 skills, 2 commands, 6 rules, 5 hooks, 13 output styles, modern CLI tools",
   "author": "0xDarkMatter",
   "repository": "https://github.com/0xDarkMatter/claude-mods",
   "license": "MIT",
@@ -136,7 +136,8 @@
       "hooks/pre-commit-lint.sh",
       "hooks/post-edit-format.sh",
       "hooks/dangerous-cmd-warn.sh",
-      "hooks/check-mail.sh"
+      "hooks/check-mail.sh",
+      "hooks/enforce-uv.sh"
     ],
     "output-styles": [
       "output-styles/vesper.md",

+ 3 - 2
README.md

@@ -12,13 +12,13 @@
 
 > *A comprehensive extension toolkit that transforms Claude Code into a specialized development powerhouse.*
 
-**claude-mods** is a production-ready plugin that extends Claude Code with 23 expert agents, 78 specialized skills, 13 output styles, 4 hooks, and modern CLI tools designed for real-world development workflows. Whether you're debugging React hooks, optimizing PostgreSQL queries, or building production CLI applications, this toolkit equips Claude with the domain expertise and procedural knowledge to work at expert level across multiple technology stacks.
+**claude-mods** is a production-ready plugin that extends Claude Code with 23 expert agents, 78 specialized skills, 13 output styles, 5 hooks, and modern CLI tools designed for real-world development workflows. Whether you're debugging React hooks, optimizing PostgreSQL queries, or building production CLI applications, this toolkit equips Claude with the domain expertise and procedural knowledge to work at expert level across multiple technology stacks.
 
 Built on the [Agent Skills specification](https://agentskills.io/specification) (an open standard backed by Anthropic, Vercel, Google, Microsoft, and 40+ agent platforms), claude-mods fills critical gaps in Claude Code's capabilities: persistent session state that survives across machines, on-demand expert knowledge for specialized domains, token-efficient modern CLI tools (10-100x faster than traditional alternatives), and proven workflow patterns for TDD, code review, and feature development. The toolkit implements Anthropic's [recommended patterns for long-running agents](https://www.anthropic.com/engineering/effective-harnesses-for-long-running-agents), ensuring your development context never vanishes when sessions end.
 
 From Python async patterns to Rust ownership models, from AWS Fargate deployments to Craft CMS development - claude-mods provides the specialized knowledge and tools that transform Claude from a general-purpose assistant into a domain expert who understands your stack, remembers your workflow, and ships production code.
 
-**23 agents. 78 skills. 13 styles. 4 hooks. 6 rules. One install.**
+**23 agents. 78 skills. 13 styles. 5 hooks. 6 rules. One install.**
 
 ## Recent Updates
 
@@ -327,6 +327,7 @@ See [skill-creator](skills/skill-creator/) for the complete guide.
 | [pre-commit-lint.sh](hooks/pre-commit-lint.sh) | PreToolUse | Auto-lint staged files before commit (JS/TS, Python, Go, Rust, PHP) |
 | [post-edit-format.sh](hooks/post-edit-format.sh) | PostToolUse | Auto-format files after Write/Edit (Prettier, Ruff, gofmt, rustfmt) |
 | [dangerous-cmd-warn.sh](hooks/dangerous-cmd-warn.sh) | PreToolUse | Block destructive commands (force push, rm -rf, DROP TABLE) |
+| [enforce-uv.sh](hooks/enforce-uv.sh) | PreToolUse | Enforce uv over pip/bare tools in uv projects (`pip install` → `uv add`, bare `pytest`/`ruff` → `uv run`) |
 | [check-mail.sh](hooks/check-mail.sh) | PreToolUse | Check for unread pmail via signal file (no cooldown, zero-cost when empty) |
 
 ### Output Styles

+ 3 - 0
hooks/README.md

@@ -9,6 +9,8 @@ Claude Code hooks allow you to run custom scripts at key workflow points.
 | `pre-commit-lint.sh` | PreToolUse | Auto-lint staged files before commit (JS/TS, Python, Go, Rust, PHP) |
 | `post-edit-format.sh` | PostToolUse | Auto-format files after Write/Edit (Prettier, Ruff, gofmt, rustfmt) |
 | `dangerous-cmd-warn.sh` | PreToolUse | Block destructive commands (force push, rm -rf, DROP TABLE, etc.) |
+| `enforce-uv.sh` | PreToolUse | Enforce uv over pip/bare tools in uv-managed projects (`pip install` → `uv add`, bare `pytest`/`ruff`/`mypy` → `uv run`) |
+| `check-mail.sh` | PreToolUse | Check for unread pigeon pmail via signal file (zero-cost when empty) |
 
 ## Configuration
 
@@ -22,6 +24,7 @@ Add hooks to `.claude/settings.json` or `.claude/settings.local.json`:
         "matcher": "Bash",
         "hooks": [
           "bash hooks/dangerous-cmd-warn.sh $TOOL_INPUT",
+          "bash hooks/enforce-uv.sh $TOOL_INPUT",
           "bash hooks/pre-commit-lint.sh $TOOL_INPUT"
         ]
       }

+ 62 - 0
hooks/enforce-uv.sh

@@ -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