spacehub

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

claude-code / plugins / ralph-wiggum / hooks / stop-hook.sh
177 lines · 6.4 KBRaw
1#!/bin/bash
2
3# Ralph Wiggum Stop Hook
4# Prevents session exit when a ralph-loop is active
5# Feeds Claude's output back as input to continue the loop
6
7set -euo pipefail
8
9# Read hook input from stdin (advanced stop hook API)
10HOOK_INPUT=$(cat)
11
12# Check if ralph-loop is active
13RALPH_STATE_FILE=".claude/ralph-loop.local.md"
14
15if [[ ! -f "$RALPH_STATE_FILE" ]]; then
16 # No active loop - allow exit
17 exit 0
18fi
19
20# Parse markdown frontmatter (YAML between ---) and extract values
21FRONTMATTER=$(sed -n '/^---$/,/^---$/{ /^---$/d; p; }' "$RALPH_STATE_FILE")
22ITERATION=$(echo "$FRONTMATTER" | grep '^iteration:' | sed 's/iteration: *//')
23MAX_ITERATIONS=$(echo "$FRONTMATTER" | grep '^max_iterations:' | sed 's/max_iterations: *//')
24# Extract completion_promise and strip surrounding quotes if present
25COMPLETION_PROMISE=$(echo "$FRONTMATTER" | grep '^completion_promise:' | sed 's/completion_promise: *//' | sed 's/^"\(.*\)"$/\1/')
26
27# Validate numeric fields before arithmetic operations
28if [[ ! "$ITERATION" =~ ^[0-9]+$ ]]; then
29 echo "⚠️ Ralph loop: State file corrupted" >&2
30 echo " File: $RALPH_STATE_FILE" >&2
31 echo " Problem: 'iteration' field is not a valid number (got: '$ITERATION')" >&2
32 echo "" >&2
33 echo " This usually means the state file was manually edited or corrupted." >&2
34 echo " Ralph loop is stopping. Run /ralph-loop again to start fresh." >&2
35 rm "$RALPH_STATE_FILE"
36 exit 0
37fi
38
39if [[ ! "$MAX_ITERATIONS" =~ ^[0-9]+$ ]]; then
40 echo "⚠️ Ralph loop: State file corrupted" >&2
41 echo " File: $RALPH_STATE_FILE" >&2
42 echo " Problem: 'max_iterations' field is not a valid number (got: '$MAX_ITERATIONS')" >&2
43 echo "" >&2
44 echo " This usually means the state file was manually edited or corrupted." >&2
45 echo " Ralph loop is stopping. Run /ralph-loop again to start fresh." >&2
46 rm "$RALPH_STATE_FILE"
47 exit 0
48fi
49
50# Check if max iterations reached
51if [[ $MAX_ITERATIONS -gt 0 ]] && [[ $ITERATION -ge $MAX_ITERATIONS ]]; then
52 echo "🛑 Ralph loop: Max iterations ($MAX_ITERATIONS) reached."
53 rm "$RALPH_STATE_FILE"
54 exit 0
55fi
56
57# Get transcript path from hook input
58TRANSCRIPT_PATH=$(echo "$HOOK_INPUT" | jq -r '.transcript_path')
59
60if [[ ! -f "$TRANSCRIPT_PATH" ]]; then
61 echo "⚠️ Ralph loop: Transcript file not found" >&2
62 echo " Expected: $TRANSCRIPT_PATH" >&2
63 echo " This is unusual and may indicate a Claude Code internal issue." >&2
64 echo " Ralph loop is stopping." >&2
65 rm "$RALPH_STATE_FILE"
66 exit 0
67fi
68
69# Read last assistant message from transcript (JSONL format - one JSON per line)
70# First check if there are any assistant messages
71if ! grep -q '"role":"assistant"' "$TRANSCRIPT_PATH"; then
72 echo "⚠️ Ralph loop: No assistant messages found in transcript" >&2
73 echo " Transcript: $TRANSCRIPT_PATH" >&2
74 echo " This is unusual and may indicate a transcript format issue" >&2
75 echo " Ralph loop is stopping." >&2
76 rm "$RALPH_STATE_FILE"
77 exit 0
78fi
79
80# Extract last assistant message with explicit error handling
81LAST_LINE=$(grep '"role":"assistant"' "$TRANSCRIPT_PATH" | tail -1)
82if [[ -z "$LAST_LINE" ]]; then
83 echo "⚠️ Ralph loop: Failed to extract last assistant message" >&2
84 echo " Ralph loop is stopping." >&2
85 rm "$RALPH_STATE_FILE"
86 exit 0
87fi
88
89# Parse JSON with error handling
90LAST_OUTPUT=$(echo "$LAST_LINE" | jq -r '
91 .message.content |
92 map(select(.type == "text")) |
93 map(.text) |
94 join("\n")
95' 2>&1)
96
97# Check if jq succeeded
98if [[ $? -ne 0 ]]; then
99 echo "⚠️ Ralph loop: Failed to parse assistant message JSON" >&2
100 echo " Error: $LAST_OUTPUT" >&2
101 echo " This may indicate a transcript format issue" >&2
102 echo " Ralph loop is stopping." >&2
103 rm "$RALPH_STATE_FILE"
104 exit 0
105fi
106
107if [[ -z "$LAST_OUTPUT" ]]; then
108 echo "⚠️ Ralph loop: Assistant message contained no text content" >&2
109 echo " Ralph loop is stopping." >&2
110 rm "$RALPH_STATE_FILE"
111 exit 0
112fi
113
114# Check for completion promise (only if set)
115if [[ "$COMPLETION_PROMISE" != "null" ]] && [[ -n "$COMPLETION_PROMISE" ]]; then
116 # Extract text from <promise> tags using Perl for multiline support
117 # -0777 slurps entire input, s flag makes . match newlines
118 # .*? is non-greedy (takes FIRST tag), whitespace normalized
119 PROMISE_TEXT=$(echo "$LAST_OUTPUT" | perl -0777 -pe 's/.*?<promise>(.*?)<\/promise>.*/$1/s; s/^\s+|\s+$//g; s/\s+/ /g' 2>/dev/null || echo "")
120
121 # Use = for literal string comparison (not pattern matching)
122 # == in [[ ]] does glob pattern matching which breaks with *, ?, [ characters
123 if [[ -n "$PROMISE_TEXT" ]] && [[ "$PROMISE_TEXT" = "$COMPLETION_PROMISE" ]]; then
124 echo "✅ Ralph loop: Detected <promise>$COMPLETION_PROMISE</promise>"
125 rm "$RALPH_STATE_FILE"
126 exit 0
127 fi
128fi
129
130# Not complete - continue loop with SAME PROMPT
131NEXT_ITERATION=$((ITERATION + 1))
132
133# Extract prompt (everything after the closing ---)
134# Skip first --- line, skip until second --- line, then print everything after
135# Use i>=2 instead of i==2 to handle --- in prompt content
136PROMPT_TEXT=$(awk '/^---$/{i++; next} i>=2' "$RALPH_STATE_FILE")
137
138if [[ -z "$PROMPT_TEXT" ]]; then
139 echo "⚠️ Ralph loop: State file corrupted or incomplete" >&2
140 echo " File: $RALPH_STATE_FILE" >&2
141 echo " Problem: No prompt text found" >&2
142 echo "" >&2
143 echo " This usually means:" >&2
144 echo " • State file was manually edited" >&2
145 echo " • File was corrupted during writing" >&2
146 echo "" >&2
147 echo " Ralph loop is stopping. Run /ralph-loop again to start fresh." >&2
148 rm "$RALPH_STATE_FILE"
149 exit 0
150fi
151
152# Update iteration in frontmatter (portable across macOS and Linux)
153# Create temp file, then atomically replace
154TEMP_FILE="${RALPH_STATE_FILE}.tmp.$$"
155sed "s/^iteration: .*/iteration: $NEXT_ITERATION/" "$RALPH_STATE_FILE" > "$TEMP_FILE"
156mv "$TEMP_FILE" "$RALPH_STATE_FILE"
157
158# Build system message with iteration count and completion promise info
159if [[ "$COMPLETION_PROMISE" != "null" ]] && [[ -n "$COMPLETION_PROMISE" ]]; then
160 SYSTEM_MSG="🔄 Ralph iteration $NEXT_ITERATION | To stop: output <promise>$COMPLETION_PROMISE</promise> (ONLY when statement is TRUE - do not lie to exit!)"
161else
162 SYSTEM_MSG="🔄 Ralph iteration $NEXT_ITERATION | No completion promise set - loop runs infinitely"
163fi
164
165# Output JSON to block the stop and feed prompt back
166# The "reason" field contains the prompt that will be sent back to Claude
167jq -n \
168 --arg prompt "$PROMPT_TEXT" \
169 --arg msg "$SYSTEM_MSG" \
170 '{
171 "decision": "block",
172 "reason": $prompt,
173 "systemMessage": $msg
174 }'
175
176# Exit 0 for successful hook execution
177exit 0