| name | py-git-hooks |
| description | Set up git pre-commit hooks to run ruff, mypy, and basedpyright before commits. Use when configuring automated quality checks in git workflow. |
| status | stable |
Python Git Hooks Setup
Configure git pre-commit hooks using the pre-commit framework to enforce code quality before commits.
Objectives
- Install pre-commit framework with Python quality hooks
- Migrate any existing manual hooks to pre-commit framework
- Configure Claude Code Stop hook lint gate (runs full lint suite before returning to user)
- Ensure hooks run incrementally on changed files only
- Auto-fix issues where possible, block on critical errors
Required Tools
Add to [dependency-groups] dev: "pre-commit", "ruff", "mypy", "basedpyright"
- pre-commit: Hook management framework (required)
- ruff: Fast linter with auto-fix capability
- mypy: Standard Python type checker
- basedpyright: Enhanced type analysis
Permissions: Run py-quality-setup first to configure .claude/settings.local.json with all needed tool permissions.
Setup Workflow
Step 1: Check for Existing Hooks
ls -la .git/hooks/pre-commit 2>/dev/null
Step 2: Create Pre-commit Configuration
Create .pre-commit-config.yaml:
repos:
- repo: https://github.com/astral-sh/ruff-pre-commit
rev: v0.8.0
hooks:
- id: ruff
args: [--fix]
- id: ruff-format
- repo: https://github.com/pre-commit/mirrors-mypy
rev: v1.13.0
hooks:
- id: mypy
additional_dependencies: []
- repo: local
hooks:
- id: basedpyright
name: basedpyright
entry: basedpyright
language: system
types: [python]
pass_filenames: true
Step 3: Install Hooks
uv pip install pre-commit
pre-commit install
ls -la .git/hooks/pre-commit
Step 4: Test Hooks
pre-commit run --all-files
git add some_file.py
pre-commit run
Step 5: Configure Claude Code Stop Hook Lint Gate
Strongly recommended: Configure a Claude Code Stop hook that runs the full lint suite (ruff, mypy, basedpyright) on modified Python files before Claude returns control to the user. If any linter reports errors, Claude is blocked from stopping and must fix them first.
This replaces the older PostToolUse approach (which ran ruff after every individual edit, generating noise on intermediate states).
Install the lint gate script (symlink so updates propagate automatically):
mkdir -p ~/.claude/hooks
ln -sf ~/.claude/skills/py-git-hooks/lint-gate.py ~/.claude/hooks/lint-gate.py
Configure the Stop hook in ~/.claude/settings.json:
{
"hooks": {
"Stop": [
{
"hooks": [
{
"type": "command",
"command": "python3 ~/.claude/hooks/lint-gate.py"
}
]
}
]
}
}
How it works:
- Fires once when Claude finishes responding, before the user sees the result
- Finds modified
.py files via git diff (staged, unstaged, and untracked)
- Auto-fix pass: runs
ruff check --fix and ruff format to silently fix trivial issues (import sorting, whitespace, style)
- Check pass: runs
ruff check, mypy, and basedpyright to find remaining unfixable errors
- If errors remain: returns
{"decision": "block", "reason": "..."} — Claude sees the lint output and continues working to fix the issues
- If clean (after auto-fix): returns
{"decision": "stop"} — Claude returns to the user
- Loop prevention: if
stop_hook_active is true in the input (meaning Claude is already retrying after a previous block), the hook allows the stop to prevent infinite loops
- Gracefully skips tools that aren't installed in the project
Benefits over PostToolUse hooks:
- Runs once per turn instead of after every edit — no noise from intermediate states
- Auto-fixes trivial issues so Claude only blocks on real errors
- Runs the full suite (ruff + mypy + basedpyright), not just ruff
- User always sees a clean codebase when Claude returns
- Claude learns from the feedback and avoids repeating the same mistakes
Migration from Manual Hooks
If the project has an existing manual .git/hooks/pre-commit script:
Step 1: Backup Existing Hook
cp .git/hooks/pre-commit .git/hooks/pre-commit.backup
Step 2: Analyze Existing Hook
Read the existing hook to understand what checks it runs:
- Linting (ruff, flake8, pylint)?
- Type checking (mypy, pyright)?
- Formatting (black, isort)?
- Custom checks?
Step 3: Map to Pre-commit Config
For each check in the manual hook, add equivalent to .pre-commit-config.yaml:
| Manual Hook Check | Pre-commit Equivalent |
|---|
ruff check | ruff-pre-commit repo |
black | ruff-format (ruff replaces black) |
isort | ruff with isort rules (I) |
flake8 | ruff (replaces flake8) |
mypy | mirrors-mypy repo |
basedpyright | local hook |
| Custom script | local hook with entry: ./script.sh |
Step 4: Install Pre-commit and Remove Manual Hook
pre-commit install
pre-commit run --all-files
Step 5: Verify and Clean Up
git add .
git commit -m "test" --dry-run
rm .git/hooks/pre-commit.backup
Hook Behavior
Auto-fixable issues (handled by both pre-commit and the Stop hook lint gate):
- Import sorting
- Trailing whitespace
- Simple style violations
- Many code quality issues
Blocking issues (require Claude or manual intervention):
- Type errors (mypy/basedpyright)
- Syntax errors
- Complex linting violations that can't be auto-fixed
Bypass (use sparingly):
git commit --no-verify
Performance Optimization
For large codebases:
- Pre-commit runs on staged files only by default
- Use mypy incremental mode (cache in
.mypy_cache)
- Consider running mypy/basedpyright in CI only for speed
For monorepos:
- Use
files: pattern in hook config to limit scope
- Configure separate hooks per package
- id: mypy
files: ^src/mypackage/
Troubleshooting
Hook not running:
pre-commit install --force
Tools not found:
uv pip install ruff mypy basedpyright
Hook too slow:
- Profile:
pre-commit run --verbose
- Consider removing mypy/basedpyright from pre-commit, run in CI instead
- Use
stages: [manual] for slow hooks, run explicitly
Update hook versions:
pre-commit autoupdate
Migration from PostToolUse Hooks
If the project was previously set up with PostToolUse hooks for ruff (the older approach), migrate to the Stop hook lint gate:
Step 1: Check for Existing PostToolUse Hooks
grep -A 10 '"PostToolUse"' ~/.claude/settings.json
Step 2: Remove PostToolUse Ruff Hooks
Edit ~/.claude/settings.json and remove any PostToolUse entries that match Edit|Write|MultiEdit and run ruff check. Keep any non-ruff PostToolUse hooks.
Step 3: Install Stop Hook
Follow Step 5 in the Setup Workflow above to install lint-gate.py and configure the Stop hook.
Step 4: Verify
Make a Python edit that introduces a ruff error. Confirm that:
- No feedback appears after the edit itself (PostToolUse hook removed)
- When Claude finishes responding, the Stop hook fires and blocks with the error
- Claude fixes the error and returns clean output
Lint Learning
Claude should learn from lint gate feedback to avoid repeating the same mistakes.
After being blocked by the lint gate: note the error pattern. If the same linter rule triggers repeatedly across edits, record it in auto-memory under the lint-patterns topic. Format:
- **[rule-code]** (tool): What triggers it and how to avoid it.
Before editing Python files: consult auto-memory for known lint pitfalls in this project. Apply those lessons proactively so the lint gate passes on the first try.
This creates a cross-session feedback loop: each lint gate block teaches Claude to write cleaner code next time.
Verification Checklist
Examples
Example: New project setup
1. Create venv: uv venv && source .venv/bin/activate
2. Install tools: uv pip install pre-commit ruff mypy basedpyright
3. Create .pre-commit-config.yaml with ruff, mypy, basedpyright
4. Install hooks: pre-commit install
5. Test: pre-commit run --all-files
6. Install lint-gate.py and configure Stop hook in settings.json
Example: Migrate from PostToolUse to Stop hook
1. Remove PostToolUse ruff hooks from ~/.claude/settings.json
2. Symlink lint-gate.py to ~/.claude/hooks/
3. Add Stop hook to ~/.claude/settings.json
4. Verify: make an edit with a ruff error, confirm block-on-stop behavior
Example: Migrate existing manual hook
1. Backup: cp .git/hooks/pre-commit .git/hooks/pre-commit.backup
2. Read backup to identify checks (ruff, mypy, custom scripts)
3. Create .pre-commit-config.yaml mapping each check
4. Install: pre-commit install (overwrites manual hook)
5. Verify: pre-commit run --all-files
6. Clean up: rm .git/hooks/pre-commit.backup
Example: Add to existing pre-commit config
1. Edit .pre-commit-config.yaml
2. Add basedpyright local hook
3. Run: pre-commit run --all-files
4. Commit updated config
Related Skills
- Prerequisites: py-quality-setup (tools must be configured before adding hooks)
- Complements: py-security (add bandit to pre-commit for security scanning)
- See also: All skills benefit from automated enforcement via hooks