| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153 |
- #!/bin/bash
- # Hook Linter
- # Checks hook scripts for common issues and best practices
- set -euo pipefail
- # Usage
- if [ $# -eq 0 ]; then
- echo "Usage: $0 <hook-script.sh> [hook-script2.sh ...]"
- echo ""
- echo "Checks hook scripts for:"
- echo " - Shebang presence"
- echo " - set -euo pipefail usage"
- echo " - Input reading from stdin"
- echo " - Proper error handling"
- echo " - Variable quoting"
- echo " - Exit code usage"
- echo " - Hardcoded paths"
- echo " - Timeout considerations"
- exit 1
- fi
- check_script() {
- local script="$1"
- local warnings=0
- local errors=0
- echo "🔍 Linting: $script"
- echo ""
- if [ ! -f "$script" ]; then
- echo "❌ Error: File not found"
- return 1
- fi
- # Check 1: Executable
- if [ ! -x "$script" ]; then
- echo "⚠️ Not executable (chmod +x $script)"
- ((warnings++))
- fi
- # Check 2: Shebang
- first_line=$(head -1 "$script")
- if [[ ! "$first_line" =~ ^#!/ ]]; then
- echo "❌ Missing shebang (#!/bin/bash)"
- ((errors++))
- fi
- # Check 3: set -euo pipefail
- if ! grep -q "set -euo pipefail" "$script"; then
- echo "⚠️ Missing 'set -euo pipefail' (recommended for safety)"
- ((warnings++))
- fi
- # Check 4: Reads from stdin
- if ! grep -q "cat\|read" "$script"; then
- echo "⚠️ Doesn't appear to read input from stdin"
- ((warnings++))
- fi
- # Check 5: Uses jq for JSON parsing
- if grep -q "tool_input\|tool_name" "$script" && ! grep -q "jq" "$script"; then
- echo "⚠️ Parses hook input but doesn't use jq"
- ((warnings++))
- fi
- # Check 6: Unquoted variables
- if grep -E '\$[A-Za-z_][A-Za-z0-9_]*[^"]' "$script" | grep -v '#' | grep -q .; then
- echo "⚠️ Potentially unquoted variables detected (injection risk)"
- echo " Always use double quotes: \"\$variable\" not \$variable"
- ((warnings++))
- fi
- # Check 7: Hardcoded paths
- if grep -E '^[^#]*/home/|^[^#]*/usr/|^[^#]*/opt/' "$script" | grep -q .; then
- echo "⚠️ Hardcoded absolute paths detected"
- echo " Use \$CLAUDE_PROJECT_DIR or \$CLAUDE_PLUGIN_ROOT"
- ((warnings++))
- fi
- # Check 8: Uses CLAUDE_PLUGIN_ROOT
- if ! grep -q "CLAUDE_PLUGIN_ROOT\|CLAUDE_PROJECT_DIR" "$script"; then
- echo "💡 Tip: Use \$CLAUDE_PLUGIN_ROOT for plugin-relative paths"
- fi
- # Check 9: Exit codes
- if ! grep -q "exit 0\|exit 2" "$script"; then
- echo "⚠️ No explicit exit codes (should exit 0 or 2)"
- ((warnings++))
- fi
- # Check 10: JSON output for decision hooks
- if grep -q "PreToolUse\|Stop" "$script"; then
- if ! grep -q "permissionDecision\|decision" "$script"; then
- echo "💡 Tip: PreToolUse/Stop hooks should output decision JSON"
- fi
- fi
- # Check 11: Long-running commands
- if grep -E 'sleep [0-9]{3,}|while true' "$script" | grep -v '#' | grep -q .; then
- echo "⚠️ Potentially long-running code detected"
- echo " Hooks should complete quickly (< 60s)"
- ((warnings++))
- fi
- # Check 12: Error messages to stderr
- if grep -q 'echo.*".*error\|Error\|denied\|Denied' "$script"; then
- if ! grep -q '>&2' "$script"; then
- echo "⚠️ Error messages should be written to stderr (>&2)"
- ((warnings++))
- fi
- fi
- # Check 13: Input validation
- if ! grep -q "if.*empty\|if.*null\|if.*-z" "$script"; then
- echo "💡 Tip: Consider validating input fields aren't empty"
- fi
- echo ""
- echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
- if [ $errors -eq 0 ] && [ $warnings -eq 0 ]; then
- echo "✅ No issues found"
- return 0
- elif [ $errors -eq 0 ]; then
- echo "⚠️ Found $warnings warning(s)"
- return 0
- else
- echo "❌ Found $errors error(s) and $warnings warning(s)"
- return 1
- fi
- }
- echo "🔎 Hook Script Linter"
- echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
- echo ""
- total_errors=0
- for script in "$@"; do
- if ! check_script "$script"; then
- ((total_errors++))
- fi
- echo ""
- done
- if [ $total_errors -eq 0 ]; then
- echo "✅ All scripts passed linting"
- exit 0
- else
- echo "❌ $total_errors script(s) had errors"
- exit 1
- fi
|