| #!/bin/bash |
| |
| |
|
|
| set -euo pipefail |
|
|
| |
| 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 |
|
|
| |
| if [ ! -x "$script" ]; then |
| echo "β οΈ Not executable (chmod +x $script)" |
| ((warnings++)) |
| fi |
|
|
| |
| first_line=$(head -1 "$script") |
| if [[ ! "$first_line" =~ ^ |
| echo "β Missing shebang (#!/bin/bash)" |
| ((errors++)) |
| fi |
|
|
| |
| if ! grep -q "set -euo pipefail" "$script"; then |
| echo "β οΈ Missing 'set -euo pipefail' (recommended for safety)" |
| ((warnings++)) |
| fi |
|
|
| |
| if ! grep -q "cat\|read" "$script"; then |
| echo "β οΈ Doesn't appear to read input from stdin" |
| ((warnings++)) |
| fi |
|
|
| |
| if grep -q "tool_input\|tool_name" "$script" && ! grep -q "jq" "$script"; then |
| echo "β οΈ Parses hook input but doesn't use jq" |
| ((warnings++)) |
| fi |
|
|
| |
| 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 |
|
|
| |
| 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 |
|
|
| |
| if ! grep -q "CLAUDE_PLUGIN_ROOT\|CLAUDE_PROJECT_DIR" "$script"; then |
| echo "π‘ Tip: Use \$CLAUDE_PLUGIN_ROOT for plugin-relative paths" |
| fi |
|
|
| |
| if ! grep -q "exit 0\|exit 2" "$script"; then |
| echo "β οΈ No explicit exit codes (should exit 0 or 2)" |
| ((warnings++)) |
| fi |
|
|
| |
| 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 |
|
|
| |
| 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 |
|
|
| |
| 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 |
|
|
| |
| 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 |
|
|