hook-linter.sh 4.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153
  1. #!/bin/bash
  2. # Hook Linter
  3. # Checks hook scripts for common issues and best practices
  4. set -euo pipefail
  5. # Usage
  6. if [ $# -eq 0 ]; then
  7. echo "Usage: $0 <hook-script.sh> [hook-script2.sh ...]"
  8. echo ""
  9. echo "Checks hook scripts for:"
  10. echo " - Shebang presence"
  11. echo " - set -euo pipefail usage"
  12. echo " - Input reading from stdin"
  13. echo " - Proper error handling"
  14. echo " - Variable quoting"
  15. echo " - Exit code usage"
  16. echo " - Hardcoded paths"
  17. echo " - Timeout considerations"
  18. exit 1
  19. fi
  20. check_script() {
  21. local script="$1"
  22. local warnings=0
  23. local errors=0
  24. echo "🔍 Linting: $script"
  25. echo ""
  26. if [ ! -f "$script" ]; then
  27. echo "❌ Error: File not found"
  28. return 1
  29. fi
  30. # Check 1: Executable
  31. if [ ! -x "$script" ]; then
  32. echo "⚠️ Not executable (chmod +x $script)"
  33. ((warnings++))
  34. fi
  35. # Check 2: Shebang
  36. first_line=$(head -1 "$script")
  37. if [[ ! "$first_line" =~ ^#!/ ]]; then
  38. echo "❌ Missing shebang (#!/bin/bash)"
  39. ((errors++))
  40. fi
  41. # Check 3: set -euo pipefail
  42. if ! grep -q "set -euo pipefail" "$script"; then
  43. echo "⚠️ Missing 'set -euo pipefail' (recommended for safety)"
  44. ((warnings++))
  45. fi
  46. # Check 4: Reads from stdin
  47. if ! grep -q "cat\|read" "$script"; then
  48. echo "⚠️ Doesn't appear to read input from stdin"
  49. ((warnings++))
  50. fi
  51. # Check 5: Uses jq for JSON parsing
  52. if grep -q "tool_input\|tool_name" "$script" && ! grep -q "jq" "$script"; then
  53. echo "⚠️ Parses hook input but doesn't use jq"
  54. ((warnings++))
  55. fi
  56. # Check 6: Unquoted variables
  57. if grep -E '\$[A-Za-z_][A-Za-z0-9_]*[^"]' "$script" | grep -v '#' | grep -q .; then
  58. echo "⚠️ Potentially unquoted variables detected (injection risk)"
  59. echo " Always use double quotes: \"\$variable\" not \$variable"
  60. ((warnings++))
  61. fi
  62. # Check 7: Hardcoded paths
  63. if grep -E '^[^#]*/home/|^[^#]*/usr/|^[^#]*/opt/' "$script" | grep -q .; then
  64. echo "⚠️ Hardcoded absolute paths detected"
  65. echo " Use \$CLAUDE_PROJECT_DIR or \$CLAUDE_PLUGIN_ROOT"
  66. ((warnings++))
  67. fi
  68. # Check 8: Uses CLAUDE_PLUGIN_ROOT
  69. if ! grep -q "CLAUDE_PLUGIN_ROOT\|CLAUDE_PROJECT_DIR" "$script"; then
  70. echo "💡 Tip: Use \$CLAUDE_PLUGIN_ROOT for plugin-relative paths"
  71. fi
  72. # Check 9: Exit codes
  73. if ! grep -q "exit 0\|exit 2" "$script"; then
  74. echo "⚠️ No explicit exit codes (should exit 0 or 2)"
  75. ((warnings++))
  76. fi
  77. # Check 10: JSON output for decision hooks
  78. if grep -q "PreToolUse\|Stop" "$script"; then
  79. if ! grep -q "permissionDecision\|decision" "$script"; then
  80. echo "💡 Tip: PreToolUse/Stop hooks should output decision JSON"
  81. fi
  82. fi
  83. # Check 11: Long-running commands
  84. if grep -E 'sleep [0-9]{3,}|while true' "$script" | grep -v '#' | grep -q .; then
  85. echo "⚠️ Potentially long-running code detected"
  86. echo " Hooks should complete quickly (< 60s)"
  87. ((warnings++))
  88. fi
  89. # Check 12: Error messages to stderr
  90. if grep -q 'echo.*".*error\|Error\|denied\|Denied' "$script"; then
  91. if ! grep -q '>&2' "$script"; then
  92. echo "⚠️ Error messages should be written to stderr (>&2)"
  93. ((warnings++))
  94. fi
  95. fi
  96. # Check 13: Input validation
  97. if ! grep -q "if.*empty\|if.*null\|if.*-z" "$script"; then
  98. echo "💡 Tip: Consider validating input fields aren't empty"
  99. fi
  100. echo ""
  101. echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
  102. if [ $errors -eq 0 ] && [ $warnings -eq 0 ]; then
  103. echo "✅ No issues found"
  104. return 0
  105. elif [ $errors -eq 0 ]; then
  106. echo "⚠️ Found $warnings warning(s)"
  107. return 0
  108. else
  109. echo "❌ Found $errors error(s) and $warnings warning(s)"
  110. return 1
  111. fi
  112. }
  113. echo "🔎 Hook Script Linter"
  114. echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
  115. echo ""
  116. total_errors=0
  117. for script in "$@"; do
  118. if ! check_script "$script"; then
  119. ((total_errors++))
  120. fi
  121. echo ""
  122. done
  123. if [ $total_errors -eq 0 ]; then
  124. echo "✅ All scripts passed linting"
  125. exit 0
  126. else
  127. echo "❌ $total_errors script(s) had errors"
  128. exit 1
  129. fi