TLDR;
ralphloop 使用了 cc 的以下三个能力,是我目前用得非常少的:
- stop hook:在监听响应结束时读了 session 内容并要求 cc 执行指定命令
- Slash command : 用于启动并调用 setup 脚本
- 通过脚本生成 prompt :在 shell 脚本中 echo 出来的即为 prompt 的一部分
- Ralph loop 并不会(也不可能)自动清除上下文,因此你需要设置自动 compact
1. 核心概念
Ralph Loop 让 Claude Code 在持续循环中运行,自主迭代工作直到任务真正完成。它解决了 Claude Code 的一个基本问题:Claude Code 以单次模式运行,即使 Claude 推理能力很强,一旦认为输出"足够好"就会停止,但实际上如果继续迭代可以做得更好。
2. 工作原理
该插件使用 Stop hook 拦截 Claude 的退出尝试,具体流程是:
- 你运行一次命令,提供任务描述和完成标志
- Claude Code 开始处理任务
- Claude 尝试退出时,Stop hook 阻止退出
- 自动将相同的提示反馈给 Claude
- 重复直到满足完成条件
关键之处在于:每次迭代不是从零开始,Claude 能看到之前尝试修改的代码库,包括 git 历史和修改的文件,形成自我修正的反馈循环。
3. 源码
Ralph Loop 通过 Claude Code slash command 提供功能。它提供了三个命令 loop, cancel, help。
3.1 数据流
setup-ralph-loop.sh stop-hook.sh
│ │
│ 创建 │ 读取
▼ ▼
┌──────────────────────────────────────────────────┐
│ .claude/ralph-loop.local.md │
├──────────────────────────────────────────────────┤
│ prompt: "Build API" ← 重新注入给 Claude │
│ completion_promise: "COMPLETE" ← 检查是否在输出中 │
│ max_iterations: 20 ← 检查是否达到上限 │
│ current_iteration: 0 ← 每次 +1 │
└──────────────────────────────────────────────────┘
职责分工
| 职责 | setup-ralph-loop.sh | stop-hook.sh |
|---|
| 创建状态文件 | ✅ | ❌ |
| 解析用户参数 | ✅ | ❌ |
| 读取状态文件 | ✅(仅显示提示) | ✅(决策逻辑) |
| 更新迭代计数 | ❌ | ✅ |
| 检查退出条件 | ❌ | ✅ |
| 重新注入提示 | ❌ | ✅ |
| 删除状态文件 | ❌ | ✅(完成时) |
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
| ---
description: "Start Ralph Loop in current session"
argument-hint: "PROMPT [--max-iterations N] [--completion-promise TEXT]"
allowed-tools: ["Bash(${CLAUDE_PLUGIN_ROOT}/scripts/setup-ralph-loop.sh:*)"]
hide-from-slash-command-tool: "true"
---
# Ralph Loop Command
Execute the setup script to initialize the Ralph loop:
```!
"${CLAUDE_PLUGIN_ROOT}/scripts/setup-ralph-loop.sh" $ARGUMENTS
```
Please work on the task. When you try to exit, the Ralph loop will feed the SAME PROMPT back to you for the next iteration. You'll see your previous work in files and git history, allowing you to iterate and improve.
CRITICAL RULE: If a completion promise is set, you may ONLY output it when the statement is completely and unequivocally TRUE. Do not output false promises to escape the loop, even if you think you're stuck or should exit for other reasons. The loop is designed to continue until genuine completion.
|
非常简单的命令,大意为:仅当通过此命令设置的 –completion-promise 符合时才停止循环,否则一直循环调用 setup-ralph-loop.sh。
1
2
3
4
5
6
7
| 执行设置脚本以初始化 Ralph 循环:
```
"${CLAUDE_PLUGIN_ROOT}/scripts/setup-ralph-loop.sh" $ARGUMENTS
```
请处理这个任务。当你尝试退出时,Ralph 循环会将相同的提示反馈给你以进行下一次迭代。你会在文件和 git 历史中看到你的先前工作,从而允许你迭代和改进。
关键规则:如果设置了完成承诺,则只有当该承诺完成且明确地为真时,才能输出它。不要输出虚假的承诺以逃避循环,即使你认为卡住了或应该出于其他原因退出。该循环设计为持续进行直到真正完成。
|
3.2 setup-ralph-loop.sh
setup-ralph-loop.sh 是 Ralph Loop 插件的初始化脚本,负责:
- 解析命令行参数
- 创建状态文件
.claude/ralph-loop.local.md - 注入首个提示给 Claude
- 显示完成承诺提示信息
调用方式:
1
| "${CLAUDE_PLUGIN_ROOT}/scripts/setup-ralph-loop.sh" "任务描述" --max-iterations 20 --completion-promise "DONE"
|
脚本接受三种参数:
| 参数 | 说明 | 默认值 | 示例 |
|---|
PROMPT | 任务描述(位置参数) | 必需 | “Build REST API” |
--max-iterations N | 最大迭代次数 | 无限制 (0) | --max-iterations 20 |
--completion-promise TEXT | 完成标志字符串 | null | --completion-promise "COMPLETE" |
这个脚本本身也没做什么神奇的事:它负责整理了一段 prompt ,然后 echo 出来!
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
| #!/bin/bash
# Ralph Loop Setup Script
# Creates state file for in-session Ralph loop
set -euo pipefail
# Parse arguments
PROMPT_PARTS=()
MAX_ITERATIONS=0
COMPLETION_PROMISE="null"
# Parse options and positional arguments
while [[ $# -gt 0 ]]; do
case $1 in
-h|--help)
cat << 'HELP_EOF'
Ralph Loop - Interactive self-referential development loop
USAGE:
/ralph-loop [PROMPT...] [OPTIONS]
ARGUMENTS:
PROMPT... Initial prompt to start the loop (can be multiple words without quotes)
OPTIONS:
--max-iterations <n> Maximum iterations before auto-stop (default: unlimited)
--completion-promise '<text>' Promise phrase (USE QUOTES for multi-word)
-h, --help Show this help message
DESCRIPTION:
Starts a Ralph Loop in your CURRENT session. The stop hook prevents
exit and feeds your output back as input until completion or iteration limit.
To signal completion, you must output: <promise>YOUR_PHRASE</promise>
Use this for:
- Interactive iteration where you want to see progress
- Tasks requiring self-correction and refinement
- Learning how Ralph works
EXAMPLES:
/ralph-loop Build a todo API --completion-promise 'DONE' --max-iterations 20
/ralph-loop --max-iterations 10 Fix the auth bug
/ralph-loop Refactor cache layer (runs forever)
/ralph-loop --completion-promise 'TASK COMPLETE' Create a REST API
STOPPING:
Only by reaching --max-iterations or detecting --completion-promise
No manual stop - Ralph runs infinitely by default!
MONITORING:
# View current iteration:
grep '^iteration:' .claude/ralph-loop.local.md
# View full state:
head -10 .claude/ralph-loop.local.md
HELP_EOF
exit 0
;;
--max-iterations)
if [[ -z "${2:-}" ]]; then
echo "❌ Error: --max-iterations requires a number argument" >&2
echo "" >&2
echo " Valid examples:" >&2
echo " --max-iterations 10" >&2
echo " --max-iterations 50" >&2
echo " --max-iterations 0 (unlimited)" >&2
echo "" >&2
echo " You provided: --max-iterations (with no number)" >&2
exit 1
fi
if ! [[ "$2" =~ ^[0-9]+$ ]]; then
echo "❌ Error: --max-iterations must be a positive integer or 0, got: $2" >&2
echo "" >&2
echo " Valid examples:" >&2
echo " --max-iterations 10" >&2
echo " --max-iterations 50" >&2
echo " --max-iterations 0 (unlimited)" >&2
echo "" >&2
echo " Invalid: decimals (10.5), negative numbers (-5), text" >&2
exit 1
fi
MAX_ITERATIONS="$2"
shift 2
;;
--completion-promise)
if [[ -z "${2:-}" ]]; then
echo "❌ Error: --completion-promise requires a text argument" >&2
echo "" >&2
echo " Valid examples:" >&2
echo " --completion-promise 'DONE'" >&2
echo " --completion-promise 'TASK COMPLETE'" >&2
echo " --completion-promise 'All tests passing'" >&2
echo "" >&2
echo " You provided: --completion-promise (with no text)" >&2
echo "" >&2
echo " Note: Multi-word promises must be quoted!" >&2
exit 1
fi
COMPLETION_PROMISE="$2"
shift 2
;;
*)
# Non-option argument - collect all as prompt parts
PROMPT_PARTS+=("$1")
shift
;;
esac
done
# Join all prompt parts with spaces
PROMPT="${PROMPT_PARTS[*]}"
# Validate prompt is non-empty
if [[ -z "$PROMPT" ]]; then
echo "❌ Error: No prompt provided" >&2
echo "" >&2
echo " Ralph needs a task description to work on." >&2
echo "" >&2
echo " Examples:" >&2
echo " /ralph-loop Build a REST API for todos" >&2
echo " /ralph-loop Fix the auth bug --max-iterations 20" >&2
echo " /ralph-loop --completion-promise 'DONE' Refactor code" >&2
echo "" >&2
echo " For all options: /ralph-loop --help" >&2
exit 1
fi
# Create state file for stop hook (markdown with YAML frontmatter)
mkdir -p .claude
# Quote completion promise for YAML if it contains special chars or is not null
if [[ -n "$COMPLETION_PROMISE" ]] && [[ "$COMPLETION_PROMISE" != "null" ]]; then
COMPLETION_PROMISE_YAML="\"$COMPLETION_PROMISE\""
else
COMPLETION_PROMISE_YAML="null"
fi
cat > .claude/ralph-loop.local.md <<EOF
---
active: true
iteration: 1
max_iterations: $MAX_ITERATIONS
completion_promise: $COMPLETION_PROMISE_YAML
started_at: "$(date -u +%Y-%m-%dT%H:%M:%SZ)"
---
$PROMPT
EOF
# Output setup message
cat <<EOF
🔄 Ralph loop activated in this session!
Iteration: 1
Max iterations: $(if [[ $MAX_ITERATIONS -gt 0 ]]; then echo $MAX_ITERATIONS; else echo "unlimited"; fi)
Completion promise: $(if [[ "$COMPLETION_PROMISE" != "null" ]]; then echo "${COMPLETION_PROMISE//\"/} (ONLY output when TRUE - do not lie!)"; else echo "none (runs forever)"; fi)
The stop hook is now active. When you try to exit, the SAME PROMPT will be
fed back to you. You'll see your previous work in files, creating a
self-referential loop where you iteratively improve on the same task.
To monitor: head -10 .claude/ralph-loop.local.md
⚠️ WARNING: This loop cannot be stopped manually! It will run infinitely
unless you set --max-iterations or --completion-promise.
🔄
EOF
# Output the initial prompt if provided
if [[ -n "$PROMPT" ]]; then
echo ""
echo "$PROMPT"
fi
# Display completion promise requirements if set
if [[ "$COMPLETION_PROMISE" != "null" ]]; then
echo ""
echo "═══════════════════════════════════════════════════════════"
echo "CRITICAL - Ralph Loop Completion Promise"
echo "═══════════════════════════════════════════════════════════"
echo ""
echo "To complete this loop, output this EXACT text:"
echo " <promise>$COMPLETION_PROMISE</promise>"
echo ""
echo "STRICT REQUIREMENTS (DO NOT VIOLATE):"
echo " ✓ Use <promise> XML tags EXACTLY as shown above"
echo " ✓ The statement MUST be completely and unequivocally TRUE"
echo " ✓ Do NOT output false statements to exit the loop"
echo " ✓ Do NOT lie even if you think you should exit"
echo ""
echo "IMPORTANT - Do not circumvent the loop:"
echo " Even if you believe you're stuck, the task is impossible,"
echo " or you've been running too long - you MUST NOT output a"
echo " false promise statement. The loop is designed to continue"
echo " until the promise is GENUINELY TRUE. Trust the process."
echo ""
echo " If the loop should stop, the promise statement will become"
echo " true naturally. Do not force it by lying."
echo "═══════════════════════════════════════════════════════════"
fi
|
4. Stop hook
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
| #!/bin/bash
# Ralph Loop Stop Hook
# Prevents session exit when a ralph-loop is active
# Feeds Claude's output back as input to continue the loop
set -euo pipefail
# Read hook input from stdin (advanced stop hook API)
HOOK_INPUT=$(cat)
# Check if ralph-loop is active
RALPH_STATE_FILE=".claude/ralph-loop.local.md"
if [[ ! -f "$RALPH_STATE_FILE" ]]; then
# No active loop - allow exit
exit 0
fi
# Parse markdown frontmatter (YAML between ---) and extract values
FRONTMATTER=$(sed -n '/^---$/,/^---$/{ /^---$/d; p; }' "$RALPH_STATE_FILE")
ITERATION=$(echo "$FRONTMATTER" | grep '^iteration:' | sed 's/iteration: *//')
MAX_ITERATIONS=$(echo "$FRONTMATTER" | grep '^max_iterations:' | sed 's/max_iterations: *//')
# Extract completion_promise and strip surrounding quotes if present
COMPLETION_PROMISE=$(echo "$FRONTMATTER" | grep '^completion_promise:' | sed 's/completion_promise: *//' | sed 's/^"\(.*\)"$/\1/')
# Validate numeric fields before arithmetic operations
if [[ ! "$ITERATION" =~ ^[0-9]+$ ]]; then
echo "⚠️ Ralph loop: State file corrupted" >&2
echo " File: $RALPH_STATE_FILE" >&2
echo " Problem: 'iteration' field is not a valid number (got: '$ITERATION')" >&2
echo "" >&2
echo " This usually means the state file was manually edited or corrupted." >&2
echo " Ralph loop is stopping. Run /ralph-loop again to start fresh." >&2
rm "$RALPH_STATE_FILE"
exit 0
fi
if [[ ! "$MAX_ITERATIONS" =~ ^[0-9]+$ ]]; then
echo "⚠️ Ralph loop: State file corrupted" >&2
echo " File: $RALPH_STATE_FILE" >&2
echo " Problem: 'max_iterations' field is not a valid number (got: '$MAX_ITERATIONS')" >&2
echo "" >&2
echo " This usually means the state file was manually edited or corrupted." >&2
echo " Ralph loop is stopping. Run /ralph-loop again to start fresh." >&2
rm "$RALPH_STATE_FILE"
exit 0
fi
# Check if max iterations reached
if [[ $MAX_ITERATIONS -gt 0 ]] && [[ $ITERATION -ge $MAX_ITERATIONS ]]; then
echo "🛑 Ralph loop: Max iterations ($MAX_ITERATIONS) reached."
rm "$RALPH_STATE_FILE"
exit 0
fi
# Get transcript path from hook input
TRANSCRIPT_PATH=$(echo "$HOOK_INPUT" | jq -r '.transcript_path')
if [[ ! -f "$TRANSCRIPT_PATH" ]]; then
echo "⚠️ Ralph loop: Transcript file not found" >&2
echo " Expected: $TRANSCRIPT_PATH" >&2
echo " This is unusual and may indicate a Claude Code internal issue." >&2
echo " Ralph loop is stopping." >&2
rm "$RALPH_STATE_FILE"
exit 0
fi
# Read last assistant message from transcript (JSONL format - one JSON per line)
# First check if there are any assistant messages
if ! grep -q '"role":"assistant"' "$TRANSCRIPT_PATH"; then
echo "⚠️ Ralph loop: No assistant messages found in transcript" >&2
echo " Transcript: $TRANSCRIPT_PATH" >&2
echo " This is unusual and may indicate a transcript format issue" >&2
echo " Ralph loop is stopping." >&2
rm "$RALPH_STATE_FILE"
exit 0
fi
# Extract last assistant message with explicit error handling
LAST_LINE=$(grep '"role":"assistant"' "$TRANSCRIPT_PATH" | tail -1)
if [[ -z "$LAST_LINE" ]]; then
echo "⚠️ Ralph loop: Failed to extract last assistant message" >&2
echo " Ralph loop is stopping." >&2
rm "$RALPH_STATE_FILE"
exit 0
fi
# Parse JSON with error handling
LAST_OUTPUT=$(echo "$LAST_LINE" | jq -r '
.message.content |
map(select(.type == "text")) |
map(.text) |
join("\n")
' 2>&1)
# Check if jq succeeded
if [[ $? -ne 0 ]]; then
echo "⚠️ Ralph loop: Failed to parse assistant message JSON" >&2
echo " Error: $LAST_OUTPUT" >&2
echo " This may indicate a transcript format issue" >&2
echo " Ralph loop is stopping." >&2
rm "$RALPH_STATE_FILE"
exit 0
fi
if [[ -z "$LAST_OUTPUT" ]]; then
echo "⚠️ Ralph loop: Assistant message contained no text content" >&2
echo " Ralph loop is stopping." >&2
rm "$RALPH_STATE_FILE"
exit 0
fi
# Check for completion promise (only if set)
if [[ "$COMPLETION_PROMISE" != "null" ]] && [[ -n "$COMPLETION_PROMISE" ]]; then
# Extract text from <promise> tags using Perl for multiline support
# -0777 slurps entire input, s flag makes . match newlines
# .*? is non-greedy (takes FIRST tag), whitespace normalized
PROMISE_TEXT=$(echo "$LAST_OUTPUT" | perl -0777 -pe 's/.*?<promise>(.*?)<\/promise>.*/$1/s; s/^\s+|\s+$//g; s/\s+/ /g' 2>/dev/null || echo "")
# Use = for literal string comparison (not pattern matching)
# == in [[ ]] does glob pattern matching which breaks with *, ?, [ characters
if [[ -n "$PROMISE_TEXT" ]] && [[ "$PROMISE_TEXT" = "$COMPLETION_PROMISE" ]]; then
echo "✅ Ralph loop: Detected <promise>$COMPLETION_PROMISE</promise>"
rm "$RALPH_STATE_FILE"
exit 0
fi
fi
# Not complete - continue loop with SAME PROMPT
NEXT_ITERATION=$((ITERATION + 1))
# Extract prompt (everything after the closing ---)
# Skip first --- line, skip until second --- line, then print everything after
# Use i>=2 instead of i==2 to handle --- in prompt content
PROMPT_TEXT=$(awk '/^---$/{i++; next} i>=2' "$RALPH_STATE_FILE")
if [[ -z "$PROMPT_TEXT" ]]; then
echo "⚠️ Ralph loop: State file corrupted or incomplete" >&2
echo " File: $RALPH_STATE_FILE" >&2
echo " Problem: No prompt text found" >&2
echo "" >&2
echo " This usually means:" >&2
echo " • State file was manually edited" >&2
echo " • File was corrupted during writing" >&2
echo "" >&2
echo " Ralph loop is stopping. Run /ralph-loop again to start fresh." >&2
rm "$RALPH_STATE_FILE"
exit 0
fi
# Update iteration in frontmatter (portable across macOS and Linux)
# Create temp file, then atomically replace
TEMP_FILE="${RALPH_STATE_FILE}.tmp.$$"
sed "s/^iteration: .*/iteration: $NEXT_ITERATION/" "$RALPH_STATE_FILE" > "$TEMP_FILE"
mv "$TEMP_FILE" "$RALPH_STATE_FILE"
# Build system message with iteration count and completion promise info
if [[ "$COMPLETION_PROMISE" != "null" ]] && [[ -n "$COMPLETION_PROMISE" ]]; then
SYSTEM_MSG="🔄 Ralph iteration $NEXT_ITERATION | To stop: output <promise>$COMPLETION_PROMISE</promise> (ONLY when statement is TRUE - do not lie to exit!)"
else
SYSTEM_MSG="🔄 Ralph iteration $NEXT_ITERATION | No completion promise set - loop runs infinitely"
fi
# Output JSON to block the stop and feed prompt back
# The "reason" field contains the prompt that will be sent back to Claude
jq -n \
--arg prompt "$PROMPT_TEXT" \
--arg msg "$SYSTEM_MSG" \
'{
"decision": "block",
"reason": $prompt,
"systemMessage": $msg
}'
# Exit 0 for successful hook execution
exit 0
|