surshar / claude-code
Claude Code is an agentic coding tool that lives in your terminal, understands your codebase, and helps you code faster by executing routine tasks, explaining complex code, and handling git workflows - all through natural language commands.
Built by Anthropic ยท github.com/anthropics
| 1 | #!/bin/bash |
| 2 | # Hook Linter |
| 3 | # Checks hook scripts for common issues and best practices |
| 4 | |
| 5 | set -euo pipefail |
| 6 | |
| 7 | # Usage |
| 8 | if [ $# -eq 0 ]; then |
| 9 | echo "Usage: $0 <hook-script.sh> [hook-script2.sh ...]" |
| 10 | echo "" |
| 11 | echo "Checks hook scripts for:" |
| 12 | echo " - Shebang presence" |
| 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 |
| 21 | fi |
| 22 | |
| 23 | check_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 | |
| 134 | echo "๐ Hook Script Linter" |
| 135 | echo "โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ" |
| 136 | echo "" |
| 137 | |
| 138 | total_errors=0 |
| 139 | |
| 140 | for script in "$@"; do |
| 141 | if ! check_script "$script"; then |
| 142 | ((total_errors++)) |
| 143 | fi |
| 144 | echo "" |
| 145 | done |
| 146 | |
| 147 | if [ $total_errors -eq 0 ]; then |
| 148 | echo "โ All scripts passed linting" |
| 149 | exit 0 |
| 150 | else |
| 151 | echo "โ $total_errors script(s) had errors" |
| 152 | exit 1 |
| 153 | fi |