diff options
| author | sillylaird <sillyfanboy@gmail.com> | 2026-09-03 00:33:59 +0000 |
|---|---|---|
| committer | sillylaird <sillyfanboy@gmail.com> | 2026-09-03 00:33:59 +0000 |
| commit | 898b52edcb47bcb3e9d6106e74ca73e74ea01e70 (patch) | |
| tree | 85c6ee5ad58b860144551184d4cf86b560c62b91 /.agents/skills/hook-development/scripts | |
| download | www-main.tar.gz www-main.zip | |
Diffstat (limited to '')
| -rw-r--r-- | .agents/skills/hook-development/scripts/README.md | 164 | ||||
| -rw-r--r-- | .agents/skills/hook-development/scripts/hook-linter.sh | 153 | ||||
| -rw-r--r-- | .agents/skills/hook-development/scripts/test-hook.sh | 252 | ||||
| -rw-r--r-- | .agents/skills/hook-development/scripts/validate-hook-schema.sh | 159 |
4 files changed, 728 insertions, 0 deletions
diff --git a/.agents/skills/hook-development/scripts/README.md b/.agents/skills/hook-development/scripts/README.md new file mode 100644 index 0000000..02a556f --- /dev/null +++ b/.agents/skills/hook-development/scripts/README.md @@ -0,0 +1,164 @@ +# Hook Development Utility Scripts + +These scripts help validate, test, and lint hook implementations before deployment. + +## validate-hook-schema.sh + +Validates `hooks.json` configuration files for correct structure and common issues. + +**Usage:** +```bash +./validate-hook-schema.sh path/to/hooks.json +``` + +**Checks:** +- Valid JSON syntax +- Required fields present +- Valid hook event names +- Proper hook types (command/prompt) +- Timeout values in valid ranges +- Hardcoded path detection +- Prompt hook event compatibility + +**Example:** +```bash +cd my-plugin +./validate-hook-schema.sh hooks/hooks.json +``` + +## test-hook.sh + +Tests individual hook scripts with sample input before deploying to Claude Code. + +**Usage:** +```bash +./test-hook.sh [options] <hook-script> <test-input.json> +``` + +**Options:** +- `-v, --verbose` - Show detailed execution information +- `-t, --timeout N` - Set timeout in seconds (default: 60) +- `--create-sample <event-type>` - Generate sample test input + +**Example:** +```bash +# Create sample test input +./test-hook.sh --create-sample PreToolUse > test-input.json + +# Test a hook script +./test-hook.sh my-hook.sh test-input.json + +# Test with verbose output and custom timeout +./test-hook.sh -v -t 30 my-hook.sh test-input.json +``` + +**Features:** +- Sets up proper environment variables (CLAUDE_PROJECT_DIR, CLAUDE_PLUGIN_ROOT) +- Measures execution time +- Validates output JSON +- Shows exit codes and their meanings +- Captures environment file output + +## hook-linter.sh + +Checks hook scripts for common issues and best practices violations. + +**Usage:** +```bash +./hook-linter.sh <hook-script.sh> [hook-script2.sh ...] +``` + +**Checks:** +- Shebang presence +- `set -euo pipefail` usage +- Stdin input reading +- Proper error handling +- Variable quoting (injection prevention) +- Exit code usage +- Hardcoded paths +- Long-running code detection +- Error output to stderr +- Input validation + +**Example:** +```bash +# Lint single script +./hook-linter.sh ../examples/validate-write.sh + +# Lint multiple scripts +./hook-linter.sh ../examples/*.sh +``` + +## Typical Workflow + +1. **Write your hook script** + ```bash + vim my-plugin/scripts/my-hook.sh + ``` + +2. **Lint the script** + ```bash + ./hook-linter.sh my-plugin/scripts/my-hook.sh + ``` + +3. **Create test input** + ```bash + ./test-hook.sh --create-sample PreToolUse > test-input.json + # Edit test-input.json as needed + ``` + +4. **Test the hook** + ```bash + ./test-hook.sh -v my-plugin/scripts/my-hook.sh test-input.json + ``` + +5. **Add to hooks.json** + ```bash + # Edit my-plugin/hooks/hooks.json + ``` + +6. **Validate configuration** + ```bash + ./validate-hook-schema.sh my-plugin/hooks/hooks.json + ``` + +7. **Test in Claude Code** + ```bash + claude --debug + ``` + +## Tips + +- Always test hooks before deploying to avoid breaking user workflows +- Use verbose mode (`-v`) to debug hook behavior +- Check the linter output for security and best practice issues +- Validate hooks.json after any changes +- Create different test inputs for various scenarios (safe operations, dangerous operations, edge cases) + +## Common Issues + +### Hook doesn't execute + +Check: +- Script has shebang (`#!/bin/bash`) +- Script is executable (`chmod +x`) +- Path in hooks.json is correct (use `${CLAUDE_PLUGIN_ROOT}`) + +### Hook times out + +- Reduce timeout in hooks.json +- Optimize hook script performance +- Remove long-running operations + +### Hook fails silently + +- Check exit codes (should be 0 or 2) +- Ensure errors go to stderr (`>&2`) +- Validate JSON output structure + +### Injection vulnerabilities + +- Always quote variables: `"$variable"` +- Use `set -euo pipefail` +- Validate all input fields +- Run the linter to catch issues diff --git a/.agents/skills/hook-development/scripts/hook-linter.sh b/.agents/skills/hook-development/scripts/hook-linter.sh new file mode 100644 index 0000000..64f6041 --- /dev/null +++ b/.agents/skills/hook-development/scripts/hook-linter.sh @@ -0,0 +1,153 @@ +#!/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 diff --git a/.agents/skills/hook-development/scripts/test-hook.sh b/.agents/skills/hook-development/scripts/test-hook.sh new file mode 100644 index 0000000..527b119 --- /dev/null +++ b/.agents/skills/hook-development/scripts/test-hook.sh @@ -0,0 +1,252 @@ +#!/bin/bash +# Hook Testing Helper +# Tests a hook with sample input and shows output + +set -euo pipefail + +# Usage +show_usage() { + echo "Usage: $0 [options] <hook-script> <test-input.json>" + echo "" + echo "Options:" + echo " -h, --help Show this help message" + echo " -v, --verbose Show detailed execution information" + echo " -t, --timeout N Set timeout in seconds (default: 60)" + echo "" + echo "Examples:" + echo " $0 validate-bash.sh test-input.json" + echo " $0 -v -t 30 validate-write.sh write-input.json" + echo "" + echo "Creates sample test input with:" + echo " $0 --create-sample <event-type>" + exit 0 +} + +# Create sample input +create_sample() { + event_type="$1" + + case "$event_type" in + PreToolUse) + cat <<'EOF' +{ + "session_id": "test-session", + "transcript_path": "/tmp/transcript.txt", + "cwd": "/tmp/test-project", + "permission_mode": "ask", + "hook_event_name": "PreToolUse", + "tool_name": "Write", + "tool_input": { + "file_path": "/tmp/test.txt", + "content": "Test content" + } +} +EOF + ;; + PostToolUse) + cat <<'EOF' +{ + "session_id": "test-session", + "transcript_path": "/tmp/transcript.txt", + "cwd": "/tmp/test-project", + "permission_mode": "ask", + "hook_event_name": "PostToolUse", + "tool_name": "Bash", + "tool_result": "Command executed successfully" +} +EOF + ;; + Stop|SubagentStop) + cat <<'EOF' +{ + "session_id": "test-session", + "transcript_path": "/tmp/transcript.txt", + "cwd": "/tmp/test-project", + "permission_mode": "ask", + "hook_event_name": "Stop", + "reason": "Task appears complete" +} +EOF + ;; + UserPromptSubmit) + cat <<'EOF' +{ + "session_id": "test-session", + "transcript_path": "/tmp/transcript.txt", + "cwd": "/tmp/test-project", + "permission_mode": "ask", + "hook_event_name": "UserPromptSubmit", + "user_prompt": "Test user prompt" +} +EOF + ;; + SessionStart|SessionEnd) + cat <<'EOF' +{ + "session_id": "test-session", + "transcript_path": "/tmp/transcript.txt", + "cwd": "/tmp/test-project", + "permission_mode": "ask", + "hook_event_name": "SessionStart" +} +EOF + ;; + *) + echo "Unknown event type: $event_type" + echo "Valid types: PreToolUse, PostToolUse, Stop, SubagentStop, UserPromptSubmit, SessionStart, SessionEnd" + exit 1 + ;; + esac +} + +# Parse arguments +VERBOSE=false +TIMEOUT=60 + +while [ $# -gt 0 ]; do + case "$1" in + -h|--help) + show_usage + ;; + -v|--verbose) + VERBOSE=true + shift + ;; + -t|--timeout) + TIMEOUT="$2" + shift 2 + ;; + --create-sample) + create_sample "$2" + exit 0 + ;; + *) + break + ;; + esac +done + +if [ $# -ne 2 ]; then + echo "Error: Missing required arguments" + echo "" + show_usage +fi + +HOOK_SCRIPT="$1" +TEST_INPUT="$2" + +# Validate inputs +if [ ! -f "$HOOK_SCRIPT" ]; then + echo "โ Error: Hook script not found: $HOOK_SCRIPT" + exit 1 +fi + +if [ ! -x "$HOOK_SCRIPT" ]; then + echo "โ ๏ธ Warning: Hook script is not executable. Attempting to run with bash..." + HOOK_SCRIPT="bash $HOOK_SCRIPT" +fi + +if [ ! -f "$TEST_INPUT" ]; then + echo "โ Error: Test input not found: $TEST_INPUT" + exit 1 +fi + +# Validate test input JSON +if ! jq empty "$TEST_INPUT" 2>/dev/null; then + echo "โ Error: Test input is not valid JSON" + exit 1 +fi + +echo "๐งช Testing hook: $HOOK_SCRIPT" +echo "๐ฅ Input: $TEST_INPUT" +echo "" + +if [ "$VERBOSE" = true ]; then + echo "Input JSON:" + jq . "$TEST_INPUT" + echo "" +fi + +# Set up environment +export CLAUDE_PROJECT_DIR="${CLAUDE_PROJECT_DIR:-/tmp/test-project}" +export CLAUDE_PLUGIN_ROOT="${CLAUDE_PLUGIN_ROOT:-$(pwd)}" +export CLAUDE_ENV_FILE="${CLAUDE_ENV_FILE:-/tmp/test-env-$$}" + +if [ "$VERBOSE" = true ]; then + echo "Environment:" + echo " CLAUDE_PROJECT_DIR=$CLAUDE_PROJECT_DIR" + echo " CLAUDE_PLUGIN_ROOT=$CLAUDE_PLUGIN_ROOT" + echo " CLAUDE_ENV_FILE=$CLAUDE_ENV_FILE" + echo "" +fi + +# Run the hook +echo "โถ๏ธ Running hook (timeout: ${TIMEOUT}s)..." +echo "" + +start_time=$(date +%s) + +set +e +output=$(timeout "$TIMEOUT" bash -c "cat '$TEST_INPUT' | $HOOK_SCRIPT" 2>&1) +exit_code=$? +set -e + +end_time=$(date +%s) +duration=$((end_time - start_time)) + +# Analyze results +echo "โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ" +echo "Results:" +echo "" +echo "Exit Code: $exit_code" +echo "Duration: ${duration}s" +echo "" + +case $exit_code in + 0) + echo "โ
Hook approved/succeeded" + ;; + 2) + echo "๐ซ Hook blocked/denied" + ;; + 124) + echo "โฑ๏ธ Hook timed out after ${TIMEOUT}s" + ;; + *) + echo "โ ๏ธ Hook returned unexpected exit code: $exit_code" + ;; +esac + +echo "" +echo "Output:" +if [ -n "$output" ]; then + echo "$output" + echo "" + + # Try to parse as JSON + if echo "$output" | jq empty 2>/dev/null; then + echo "Parsed JSON output:" + echo "$output" | jq . + fi +else + echo "(no output)" +fi + +# Check for environment file +if [ -f "$CLAUDE_ENV_FILE" ]; then + echo "" + echo "Environment file created:" + cat "$CLAUDE_ENV_FILE" + rm -f "$CLAUDE_ENV_FILE" +fi + +echo "" +echo "โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ" + +if [ $exit_code -eq 0 ] || [ $exit_code -eq 2 ]; then + echo "โ
Test completed successfully" + exit 0 +else + echo "โ Test failed" + exit 1 +fi diff --git a/.agents/skills/hook-development/scripts/validate-hook-schema.sh b/.agents/skills/hook-development/scripts/validate-hook-schema.sh new file mode 100644 index 0000000..fed0a1f --- /dev/null +++ b/.agents/skills/hook-development/scripts/validate-hook-schema.sh @@ -0,0 +1,159 @@ +#!/bin/bash +# Hook Schema Validator +# Validates hooks.json structure and checks for common issues + +set -euo pipefail + +# Usage +if [ $# -eq 0 ]; then + echo "Usage: $0 <path/to/hooks.json>" + echo "" + echo "Validates hook configuration file for:" + echo " - Valid JSON syntax" + echo " - Required fields" + echo " - Hook type validity" + echo " - Matcher patterns" + echo " - Timeout ranges" + exit 1 +fi + +HOOKS_FILE="$1" + +if [ ! -f "$HOOKS_FILE" ]; then + echo "โ Error: File not found: $HOOKS_FILE" + exit 1 +fi + +echo "๐ Validating hooks configuration: $HOOKS_FILE" +echo "" + +# Check 1: Valid JSON +echo "Checking JSON syntax..." +if ! jq empty "$HOOKS_FILE" 2>/dev/null; then + echo "โ Invalid JSON syntax" + exit 1 +fi +echo "โ
Valid JSON" + +# Check 2: Root structure +echo "" +echo "Checking root structure..." +VALID_EVENTS=("PreToolUse" "PostToolUse" "UserPromptSubmit" "Stop" "SubagentStop" "SessionStart" "SessionEnd" "PreCompact" "Notification") + +for event in $(jq -r 'keys[]' "$HOOKS_FILE"); do + found=false + for valid_event in "${VALID_EVENTS[@]}"; do + if [ "$event" = "$valid_event" ]; then + found=true + break + fi + done + + if [ "$found" = false ]; then + echo "โ ๏ธ Unknown event type: $event" + fi +done +echo "โ
Root structure valid" + +# Check 3: Validate each hook +echo "" +echo "Validating individual hooks..." + +error_count=0 +warning_count=0 + +for event in $(jq -r 'keys[]' "$HOOKS_FILE"); do + hook_count=$(jq -r ".\"$event\" | length" "$HOOKS_FILE") + + for ((i=0; i<hook_count; i++)); do + # Check matcher exists + matcher=$(jq -r ".\"$event\"[$i].matcher // empty" "$HOOKS_FILE") + if [ -z "$matcher" ]; then + echo "โ $event[$i]: Missing 'matcher' field" + ((error_count++)) + continue + fi + + # Check hooks array exists + hooks=$(jq -r ".\"$event\"[$i].hooks // empty" "$HOOKS_FILE") + if [ -z "$hooks" ] || [ "$hooks" = "null" ]; then + echo "โ $event[$i]: Missing 'hooks' array" + ((error_count++)) + continue + fi + + # Validate each hook in the array + hook_array_count=$(jq -r ".\"$event\"[$i].hooks | length" "$HOOKS_FILE") + + for ((j=0; j<hook_array_count; j++)); do + hook_type=$(jq -r ".\"$event\"[$i].hooks[$j].type // empty" "$HOOKS_FILE") + + if [ -z "$hook_type" ]; then + echo "โ $event[$i].hooks[$j]: Missing 'type' field" + ((error_count++)) + continue + fi + + if [ "$hook_type" != "command" ] && [ "$hook_type" != "prompt" ]; then + echo "โ $event[$i].hooks[$j]: Invalid type '$hook_type' (must be 'command' or 'prompt')" + ((error_count++)) + continue + fi + + # Check type-specific fields + if [ "$hook_type" = "command" ]; then + command=$(jq -r ".\"$event\"[$i].hooks[$j].command // empty" "$HOOKS_FILE") + if [ -z "$command" ]; then + echo "โ $event[$i].hooks[$j]: Command hooks must have 'command' field" + ((error_count++)) + else + # Check for hardcoded paths + if [[ "$command" == /* ]] && [[ "$command" != *'${CLAUDE_PLUGIN_ROOT}'* ]]; then + echo "โ ๏ธ $event[$i].hooks[$j]: Hardcoded absolute path detected. Consider using \${CLAUDE_PLUGIN_ROOT}" + ((warning_count++)) + fi + fi + elif [ "$hook_type" = "prompt" ]; then + prompt=$(jq -r ".\"$event\"[$i].hooks[$j].prompt // empty" "$HOOKS_FILE") + if [ -z "$prompt" ]; then + echo "โ $event[$i].hooks[$j]: Prompt hooks must have 'prompt' field" + ((error_count++)) + fi + + # Check if prompt-based hooks are used on supported events + if [ "$event" != "Stop" ] && [ "$event" != "SubagentStop" ] && [ "$event" != "UserPromptSubmit" ] && [ "$event" != "PreToolUse" ]; then + echo "โ ๏ธ $event[$i].hooks[$j]: Prompt hooks may not be fully supported on $event (best on Stop, SubagentStop, UserPromptSubmit, PreToolUse)" + ((warning_count++)) + fi + fi + + # Check timeout + timeout=$(jq -r ".\"$event\"[$i].hooks[$j].timeout // empty" "$HOOKS_FILE") + if [ -n "$timeout" ] && [ "$timeout" != "null" ]; then + if ! [[ "$timeout" =~ ^[0-9]+$ ]]; then + echo "โ $event[$i].hooks[$j]: Timeout must be a number" + ((error_count++)) + elif [ "$timeout" -gt 600 ]; then + echo "โ ๏ธ $event[$i].hooks[$j]: Timeout $timeout seconds is very high (max 600s)" + ((warning_count++)) + elif [ "$timeout" -lt 5 ]; then + echo "โ ๏ธ $event[$i].hooks[$j]: Timeout $timeout seconds is very low" + ((warning_count++)) + fi + fi + done + done +done + +echo "" +echo "โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ" +if [ $error_count -eq 0 ] && [ $warning_count -eq 0 ]; then + echo "โ
All checks passed!" + exit 0 +elif [ $error_count -eq 0 ]; then + echo "โ ๏ธ Validation passed with $warning_count warning(s)" + exit 0 +else + echo "โ Validation failed with $error_count error(s) and $warning_count warning(s)" + exit 1 +fi |
