|
1 | 1 | #!/bin/bash |
2 | 2 | # install-git-hooks.sh - Install git hooks for multi-agent-ralph-loop |
3 | | -# VERSION: 2.57.3 |
| 3 | +# VERSION: 3.2.0 |
| 4 | +# |
| 5 | +# Installs the pre-commit hook from the SINGLE versioned source of truth |
| 6 | +# (scripts/pre-commit-template.sh) into the active hooks directory resolved from |
| 7 | +# `git config core.hooksPath`. |
| 8 | +# |
| 9 | +# This repo uses core.hooksPath=.git-hooks, which is .gitignored — the active |
| 10 | +# hook copy is LOCAL ONLY and is never committed. That is deliberate: it keeps |
| 11 | +# hook contents out of the PUBLIC repo (no risk of leaking machine-specific or |
| 12 | +# sensitive data). The committed source of truth is the template; run this |
| 13 | +# script after cloning to (re)generate the active hook locally. |
| 14 | +# |
| 15 | +# Replaces the previous embedded-heredoc approach (which had drifted to an old |
| 16 | +# version and missed Phase 1b). One source of truth now: the template. |
4 | 17 | # |
5 | 18 | # Usage: |
6 | 19 | # ./scripts/install-git-hooks.sh |
7 | | -# |
8 | | -# This installs the pre-commit hook that validates Claude Code hook JSON formats |
9 | 20 |
|
10 | 21 | set -euo pipefail |
11 | 22 |
|
12 | 23 | SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" |
13 | 24 | PROJECT_DIR="$(dirname "$SCRIPT_DIR")" |
14 | | -HOOKS_DIR="$PROJECT_DIR/.git/hooks" |
| 25 | +TEMPLATE="$SCRIPT_DIR/pre-commit-template.sh" |
15 | 26 |
|
16 | 27 | echo "Installing git hooks for multi-agent-ralph-loop..." |
17 | 28 |
|
18 | | -# Create hooks directory if it doesn't exist |
19 | | -mkdir -p "$HOOKS_DIR" |
20 | | - |
21 | | -# Create pre-commit hook |
22 | | -cat > "$HOOKS_DIR/pre-commit" << 'HOOK_EOF' |
23 | | -#!/bin/bash |
24 | | -# pre-commit hook for multi-agent-ralph-loop |
25 | | -# VERSION: 2.57.3 |
26 | | -# Purpose: Validate hook JSON formats before commit |
27 | | -# |
28 | | -# CRITICAL FORMAT RULES (per official Claude Code docs): |
29 | | -# - PostToolUse/PreToolUse/UserPromptSubmit: {"continue": true/false} |
30 | | -# - Stop hooks ONLY: {"decision": "approve"/"block"} |
31 | | -# - The string "continue" is NEVER valid for the "decision" field |
32 | | -
|
33 | | -set -uo pipefail |
34 | | -
|
35 | | -RED='\033[0;31m' |
36 | | -GREEN='\033[0;32m' |
37 | | -YELLOW='\033[0;33m' |
38 | | -NC='\033[0m' |
39 | | -
|
40 | | -STAGED_HOOKS=$(git diff --cached --name-only --diff-filter=ACM | grep -E '\.claude/hooks/.*\.(sh|py)$' || true) |
41 | | -
|
42 | | -if [[ -z "$STAGED_HOOKS" ]]; then |
43 | | - exit 0 |
| 29 | +if [[ ! -f "$TEMPLATE" ]]; then |
| 30 | + echo "ERROR: missing template: $TEMPLATE" >&2 |
| 31 | + exit 1 |
44 | 32 | fi |
45 | 33 |
|
46 | | -echo -e "${YELLOW}Pre-commit: Validating Claude Code hook JSON formats${NC}" |
47 | | -
|
48 | | -ERRORS=0 |
49 | | -
|
50 | | -for hook_file in $STAGED_HOOKS; do |
51 | | - [[ ! -f "$hook_file" ]] && continue |
52 | | -
|
53 | | - hook_name=$(basename "$hook_file") |
54 | | -
|
55 | | - # CRITICAL: "decision": "continue" is NEVER valid |
56 | | - if grep -qE '"decision":\s*"continue"' "$hook_file"; then |
57 | | - echo -e "${RED}✗ $hook_name: Uses invalid {\"decision\": \"continue\"}${NC}" |
58 | | - ((ERRORS++)) |
59 | | - continue |
60 | | - fi |
61 | | -
|
62 | | - echo -e "${GREEN}✓ $hook_name${NC}" |
63 | | -done |
64 | | -
|
65 | | -# Phase 1b: PreToolUse permissionDecision enum guard (allow|deny|ask, never "block"). |
66 | | -# Regression guard for the "(root): Invalid input" hook error. |
67 | | -PD_CHECK="$(git rev-parse --show-toplevel 2>/dev/null)/scripts/check-pretooluse-permission-decision.sh" |
68 | | -if [[ -n "$STAGED_HOOKS" && -x "$PD_CHECK" ]]; then |
69 | | - if ! "$PD_CHECK" > /dev/null 2>&1; then |
70 | | - echo -e "${RED}✗ Invalid permissionDecision (use \"deny\", not \"block\")${NC}" |
71 | | - echo " Run: $PD_CHECK" |
72 | | - ((ERRORS++)) |
73 | | - fi |
| 34 | +# Resolve the active hooks path. Honor an existing core.hooksPath; otherwise |
| 35 | +# adopt the repo convention (.git-hooks) and set it so git actually uses it. |
| 36 | +HOOKS_PATH="$(git -C "$PROJECT_DIR" config --get core.hooksPath 2>/dev/null || true)" |
| 37 | +if [[ -z "$HOOKS_PATH" ]]; then |
| 38 | + HOOKS_PATH=".git-hooks" |
| 39 | + git -C "$PROJECT_DIR" config core.hooksPath "$HOOKS_PATH" |
| 40 | + echo " core.hooksPath set to $HOOKS_PATH" |
74 | 41 | fi |
75 | 42 |
|
76 | | -if [[ $ERRORS -gt 0 ]]; then |
77 | | - echo -e "${RED}COMMIT BLOCKED: $ERRORS hook(s) have invalid JSON format${NC}" |
78 | | - echo "Reference: tests/HOOK_FORMAT_REFERENCE.md" |
79 | | - exit 1 |
80 | | -fi |
| 43 | +# Resolve to an absolute directory (core.hooksPath may be relative to repo root). |
| 44 | +case "$HOOKS_PATH" in |
| 45 | + /*) HOOKS_DIR="$HOOKS_PATH" ;; |
| 46 | + *) HOOKS_DIR="$PROJECT_DIR/$HOOKS_PATH" ;; |
| 47 | +esac |
81 | 48 |
|
82 | | -exit 0 |
83 | | -HOOK_EOF |
| 49 | +mkdir -p "$HOOKS_DIR" |
84 | 50 |
|
85 | | -chmod +x "$HOOKS_DIR/pre-commit" |
| 51 | +# Install from the single source of truth. The template already includes |
| 52 | +# Phase 1b (PreToolUse permissionDecision guard: allow|deny|ask, never "block"). |
| 53 | +install -m 0755 "$TEMPLATE" "$HOOKS_DIR/pre-commit" |
86 | 54 |
|
87 | | -echo "✓ pre-commit hook installed" |
| 55 | +echo "✓ pre-commit installed → $HOOKS_DIR/pre-commit" |
| 56 | +echo " source: scripts/pre-commit-template.sh" |
| 57 | +echo " active core.hooksPath: $(git -C "$PROJECT_DIR" config --get core.hooksPath)" |
88 | 58 | echo "" |
89 | 59 | echo "Git hooks installed successfully!" |
90 | | -echo "" |
91 | | -echo "The pre-commit hook will validate Claude Code hook JSON formats" |
92 | | -echo "before each commit to prevent format errors." |
| 60 | +echo "The pre-commit hook validates Claude Code hook JSON formats (incl. the" |
| 61 | +echo "PreToolUse permissionDecision enum) before each commit." |
93 | 62 | echo "" |
94 | 63 | echo "Reference: tests/HOOK_FORMAT_REFERENCE.md" |
0 commit comments