py-git-hooks
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
# Check if manual hooks exist
ls -la .git/hooks/pre-commit 2>/dev/null
# If exists and not a pre-commit managed hook, migrate it (see Migration section)
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: [] # Add type stubs as needed
- repo: local
hooks:
- id: basedpyright
name: basedpyright
entry: basedpyright
language: system
types: [python]
pass_filenames: true
Step 3: Install Hooks
# Install pre-commit to project venv
uv pip install pre-commit
# Install git hooks
pre-commit install
# Verify installation
ls -la .git/hooks/pre-commit
Step 4: Test Hooks
# Run on all files (initial validation)
pre-commit run --all-files
# Or test on staged files only
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
.pyfiles viagit diff(staged, unstaged, and untracked) - Auto-fix pass: runs
ruff check --fixandruff formatto silently fix trivial issues (import sorting, whitespace, style) - Check pass: runs
ruff check,mypy, andbasedpyrightto 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_activeis 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 # This overwrites .git/hooks/pre-commit
pre-commit run --all-files # Verify all checks pass
Step 5: Verify and Clean Up
# Test that hooks work
git add .
git commit -m "test" --dry-run
# If successful, remove backup
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 # Reinstall hooks
Tools not found:
# Ensure tools installed in venv
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
-
.pre-commit-config.yamlexists with ruff, mypy, basedpyright hooks -
pre-commit installhas been run -
pre-commit run --all-filespasses - Any existing manual hooks have been migrated
-
~/.claude/hooks/lint-gate.pyinstalled and executable - Claude Code Stop hook configured in
~/.claude/settings.json - Any old PostToolUse ruff hooks removed from settings
- Bypass with
--no-verifyworks for emergencies (git pre-commit only)
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