| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136 |
- #!/bin/bash
- # hooks/pre-commit-lint.sh
- # PreToolUse hook - runs linter on staged files before commit
- # Matcher: Bash (when command contains "git commit")
- #
- # Configuration in .claude/settings.json:
- # {
- # "hooks": {
- # "PreToolUse": [{
- # "matcher": "Bash",
- # "hooks": ["bash hooks/pre-commit-lint.sh $TOOL_INPUT"]
- # }]
- # }
- # }
- INPUT="$1"
- # Modern Claude Code delivers the tool call as JSON on stdin
- # ({"tool_input":{"command":"..."}}); older configs pass it as $TOOL_INPUT/$1.
- # Support both so the hook works regardless of harness version.
- if [[ -z "$INPUT" && ! -t 0 ]]; then
- RAW="$(cat 2>/dev/null)"
- if [[ -n "$RAW" ]] && command -v jq >/dev/null 2>&1; then
- INPUT="$(printf '%s' "$RAW" | jq -r '.tool_input.command // .tool_input // empty' 2>/dev/null)"
- fi
- [[ -z "$INPUT" ]] && INPUT="$RAW"
- fi
- [[ -z "$INPUT" ]] && exit 0
- # Only trigger on git commit commands
- if ! echo "$INPUT" | grep -qE 'git\s+commit'; then
- exit 0
- fi
- # Collect staged files
- STAGED_FILES=$(git diff --cached --name-only --diff-filter=ACMR 2>/dev/null)
- if [[ -z "$STAGED_FILES" ]]; then
- exit 0
- fi
- ERRORS=0
- lint_js() {
- local files
- files=$(echo "$STAGED_FILES" | grep -E '\.(ts|tsx|js|jsx|mjs|cjs)$')
- if [[ -n "$files" ]]; then
- if command -v npx &>/dev/null && [[ -f "node_modules/.bin/eslint" ]]; then
- echo "Linting JS/TS files..."
- echo "$files" | xargs npx eslint --max-warnings 0 2>/dev/null
- return $?
- elif command -v biome &>/dev/null; then
- echo "Linting JS/TS files with Biome..."
- echo "$files" | xargs biome check 2>/dev/null
- return $?
- fi
- fi
- return 0
- }
- lint_python() {
- local files
- files=$(echo "$STAGED_FILES" | grep -E '\.py$')
- if [[ -n "$files" ]]; then
- if command -v ruff &>/dev/null; then
- echo "Linting Python files..."
- echo "$files" | xargs ruff check 2>/dev/null
- return $?
- elif command -v flake8 &>/dev/null; then
- echo "$files" | xargs flake8 2>/dev/null
- return $?
- fi
- fi
- return 0
- }
- lint_go() {
- local files
- files=$(echo "$STAGED_FILES" | grep -E '\.go$')
- if [[ -n "$files" ]]; then
- if command -v golangci-lint &>/dev/null; then
- echo "Linting Go files..."
- golangci-lint run --new-from-rev=HEAD 2>/dev/null
- return $?
- elif command -v go &>/dev/null; then
- go vet ./... 2>/dev/null
- return $?
- fi
- fi
- return 0
- }
- lint_rust() {
- local files
- files=$(echo "$STAGED_FILES" | grep -E '\.rs$')
- if [[ -n "$files" ]]; then
- if command -v cargo &>/dev/null && [[ -f "Cargo.toml" ]]; then
- echo "Linting Rust files..."
- cargo clippy --all-targets -- -D warnings 2>/dev/null
- return $?
- fi
- fi
- return 0
- }
- lint_php() {
- local files
- files=$(echo "$STAGED_FILES" | grep -E '\.php$')
- if [[ -n "$files" ]]; then
- if command -v ./vendor/bin/pint &>/dev/null; then
- echo "Linting PHP files..."
- echo "$files" | xargs ./vendor/bin/pint --test 2>/dev/null
- return $?
- elif command -v php &>/dev/null; then
- for f in $files; do
- php -l "$f" 2>/dev/null || return 1
- done
- fi
- fi
- return 0
- }
- # Run all applicable linters
- lint_js || ERRORS=$((ERRORS + 1))
- lint_python || ERRORS=$((ERRORS + 1))
- lint_go || ERRORS=$((ERRORS + 1))
- lint_rust || ERRORS=$((ERRORS + 1))
- lint_php || ERRORS=$((ERRORS + 1))
- if [[ $ERRORS -gt 0 ]]; then
- echo ""
- echo "LINT FAILED: $ERRORS linter(s) reported issues."
- echo "Fix the issues above before committing."
- exit 1
- fi
- exit 0
|