pre-commit-lint.sh 3.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136
  1. #!/bin/bash
  2. # hooks/pre-commit-lint.sh
  3. # PreToolUse hook - runs linter on staged files before commit
  4. # Matcher: Bash (when command contains "git commit")
  5. #
  6. # Configuration in .claude/settings.json:
  7. # {
  8. # "hooks": {
  9. # "PreToolUse": [{
  10. # "matcher": "Bash",
  11. # "hooks": ["bash hooks/pre-commit-lint.sh $TOOL_INPUT"]
  12. # }]
  13. # }
  14. # }
  15. INPUT="$1"
  16. # Modern Claude Code delivers the tool call as JSON on stdin
  17. # ({"tool_input":{"command":"..."}}); older configs pass it as $TOOL_INPUT/$1.
  18. # Support both so the hook works regardless of harness version.
  19. if [[ -z "$INPUT" && ! -t 0 ]]; then
  20. RAW="$(cat 2>/dev/null)"
  21. if [[ -n "$RAW" ]] && command -v jq >/dev/null 2>&1; then
  22. INPUT="$(printf '%s' "$RAW" | jq -r '.tool_input.command // .tool_input // empty' 2>/dev/null)"
  23. fi
  24. [[ -z "$INPUT" ]] && INPUT="$RAW"
  25. fi
  26. [[ -z "$INPUT" ]] && exit 0
  27. # Only trigger on git commit commands
  28. if ! echo "$INPUT" | grep -qE 'git\s+commit'; then
  29. exit 0
  30. fi
  31. # Collect staged files
  32. STAGED_FILES=$(git diff --cached --name-only --diff-filter=ACMR 2>/dev/null)
  33. if [[ -z "$STAGED_FILES" ]]; then
  34. exit 0
  35. fi
  36. ERRORS=0
  37. lint_js() {
  38. local files
  39. files=$(echo "$STAGED_FILES" | grep -E '\.(ts|tsx|js|jsx|mjs|cjs)$')
  40. if [[ -n "$files" ]]; then
  41. if command -v npx &>/dev/null && [[ -f "node_modules/.bin/eslint" ]]; then
  42. echo "Linting JS/TS files..."
  43. echo "$files" | xargs npx eslint --max-warnings 0 2>/dev/null
  44. return $?
  45. elif command -v biome &>/dev/null; then
  46. echo "Linting JS/TS files with Biome..."
  47. echo "$files" | xargs biome check 2>/dev/null
  48. return $?
  49. fi
  50. fi
  51. return 0
  52. }
  53. lint_python() {
  54. local files
  55. files=$(echo "$STAGED_FILES" | grep -E '\.py$')
  56. if [[ -n "$files" ]]; then
  57. if command -v ruff &>/dev/null; then
  58. echo "Linting Python files..."
  59. echo "$files" | xargs ruff check 2>/dev/null
  60. return $?
  61. elif command -v flake8 &>/dev/null; then
  62. echo "$files" | xargs flake8 2>/dev/null
  63. return $?
  64. fi
  65. fi
  66. return 0
  67. }
  68. lint_go() {
  69. local files
  70. files=$(echo "$STAGED_FILES" | grep -E '\.go$')
  71. if [[ -n "$files" ]]; then
  72. if command -v golangci-lint &>/dev/null; then
  73. echo "Linting Go files..."
  74. golangci-lint run --new-from-rev=HEAD 2>/dev/null
  75. return $?
  76. elif command -v go &>/dev/null; then
  77. go vet ./... 2>/dev/null
  78. return $?
  79. fi
  80. fi
  81. return 0
  82. }
  83. lint_rust() {
  84. local files
  85. files=$(echo "$STAGED_FILES" | grep -E '\.rs$')
  86. if [[ -n "$files" ]]; then
  87. if command -v cargo &>/dev/null && [[ -f "Cargo.toml" ]]; then
  88. echo "Linting Rust files..."
  89. cargo clippy --all-targets -- -D warnings 2>/dev/null
  90. return $?
  91. fi
  92. fi
  93. return 0
  94. }
  95. lint_php() {
  96. local files
  97. files=$(echo "$STAGED_FILES" | grep -E '\.php$')
  98. if [[ -n "$files" ]]; then
  99. if command -v ./vendor/bin/pint &>/dev/null; then
  100. echo "Linting PHP files..."
  101. echo "$files" | xargs ./vendor/bin/pint --test 2>/dev/null
  102. return $?
  103. elif command -v php &>/dev/null; then
  104. for f in $files; do
  105. php -l "$f" 2>/dev/null || return 1
  106. done
  107. fi
  108. fi
  109. return 0
  110. }
  111. # Run all applicable linters
  112. lint_js || ERRORS=$((ERRORS + 1))
  113. lint_python || ERRORS=$((ERRORS + 1))
  114. lint_go || ERRORS=$((ERRORS + 1))
  115. lint_rust || ERRORS=$((ERRORS + 1))
  116. lint_php || ERRORS=$((ERRORS + 1))
  117. if [[ $ERRORS -gt 0 ]]; then
  118. echo ""
  119. echo "LINT FAILED: $ERRORS linter(s) reported issues."
  120. echo "Fix the issues above before committing."
  121. exit 1
  122. fi
  123. exit 0