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