NAME
Ralph Wiggum loops for LLM coding
Leveraging Ralph Wiggum bash loops for streamlining development against large task lists
SYNOPSIS
I’m a bit late on this one, but I read about Ralph Wiggum loops and became interested. The concept is pretty straightforward: the longer you run a session, the bigger the context window gets, and the more likely you are to receive hallucinations.
Instead, we craft a detailed PRD, use that to generate tasks, and then craft a prompt for the loop. This gives claude all of the context it needs to start fresh, but the activity logs lets it know what we’ve done recently as well. You include testing criteria so it won’t move forward in error, and you have a pretty good approach.
Creating the Task List
After I have a PRD that I like, I tell claude to analyze it and create the tasks. Here is an example prompt I used recently to do this. It then updated the plan.md file for me to use in my later prompt.
Review the @[prd-file] and then create a new series of tasks to accomplish it. we should then analyze the complexity of each task to determine if it needs to be subtasks. lets make sure we include tests for each task to ensure that we got it right. write the tasks out to plan.md
Ralph
I took the basic loop approach, but I also wanted to be able to see the activity along the way so I knew what was happening in the background. I did some basic tailing with jq initially, but ultimately created a method that used inotify to pickup on file changes, and display it in the foreground.
ralph.sh
#!/bin/bash
set -euo pipefail
# Detect project path from current directory
cwd=$(pwd)
proj_name=$(echo "$cwd" | tr '/' '-')
proj="$HOME/.claude/projects/$proj_name"
# Verify it exists
if [[ ! -d "$proj" ]]; then
echo "❌ No Claude project found for: $cwd"
echo " Expected: $proj"
echo ""
echo " Have you run claude in this directory before?"
exit 1
fi
max_iterations="${1:-10}"
# Pids to track
tail_pid=""
inotify_pid=""
cleanup() {
echo -e "\n🛑 Stopping..."
[[ -n "$tail_pid" ]] && kill "$tail_pid" 2>/dev/null
[[ -n "$inotify_pid" ]] && kill "$inotify_pid" 2>/dev/null
rm -f "/tmp/ralph-latest-file-$$" "/tmp/ralph-result-$$.txt"
wait 2>/dev/null
exit 0
}
trap cleanup SIGINT SIGTERM EXIT
jq_filter='
if .type == "assistant" then
.message.content[]? |
if .type == "text" then "💬 \(.text[0:200])"
elif .type == "tool_use" then
if .name == "Write" then "📝 Write: \(.input.file_path)"
elif .name == "Read" then "👁️ Read: \(.input.file_path)"
elif .name == "Bash" then "💻 Bash: \(.input.command[0:120])"
elif .name == "TodoWrite" then "📋 Todo: \(.input.todos[0].content[0:80] // "update")"
else "🔧 \(.name)"
end
else empty end
elif .type == "user" then
.message.content[]? | select(.type == "tool_result") |
if .is_error then " ❌ \(.content)"
else " ✅"
end
else empty end
'
current_file=""
start_tail() {
local file="$1"
[[ "$file" == "$current_file" ]] && return
[[ -n "$tail_pid" ]] && kill "$tail_pid" 2>/dev/null
echo ""
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
echo "📂 $(basename "$file")"
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
tail -f "$file" 2>/dev/null | jq -r --unbuffered "$jq_filter" &
tail_pid=$!
current_file="$file"
}
start_watcher() {
(
inotifywait -m -q -e create -e modify -e moved_to "$proj" --format '%f' 2>/dev/null | while read file; do
if [[ "$file" == *.jsonl ]]; then
echo "$proj/$file" > "/tmp/ralph-latest-file-$$"
fi
done
) &
inotify_pid=$!
}
check_for_new_file() {
if [[ -f "/tmp/ralph-latest-file-$$" ]]; then
local new_file
new_file=$(cat "/tmp/ralph-latest-file-$$")
if [[ "$new_file" != "$current_file" && -f "$new_file" ]]; then
start_tail "$new_file"
fi
fi
}
# --- Main ---
echo "🚀 Ralph Wiggum Loop"
echo " Directory: $cwd"
echo " Project: $proj"
echo " Max iterations: $max_iterations"
echo ""
# Check for PROMPT.md
if [[ ! -f "PROMPT.md" ]]; then
echo "❌ No PROMPT.md found in current directory"
exit 1
fi
# Start watching for file changes
rm -f "/tmp/ralph-latest-file-$$"
start_watcher
# Seed with latest existing file
latest=$(ls -1t "$proj"/*.jsonl 2>/dev/null | head -1 || true)
[[ -n "$latest" ]] && start_tail "$latest"
for ((i=1; i<=max_iterations; i++)); do
echo ""
echo "╔══════════════════════════════════════════════════════╗"
echo "║ 🔄 Iteration $i / $max_iterations"
echo "╚══════════════════════════════════════════════════════╝"
# Run claude in background so we can monitor file changes
claude --dangerously-skip-permissions -p "$(cat PROMPT.md)" --output-format text > "/tmp/ralph-result-$$.txt" 2>&1 &
claude_pid=$!
# Poll while claude runs
while kill -0 "$claude_pid" 2>/dev/null; do
check_for_new_file
sleep 1
done
# Get result
wait "$claude_pid" || true
result=$(cat "/tmp/ralph-result-$$.txt")
# Check exit conditions
if [[ "$result" == *"<promise>COMPLETE</promise>"* ]]; then
echo ""
echo "✅ All tasks complete after $i iterations!"
exit 0
elif [[ "$result" == *"<promise>BLOCKED:"* ]]; then
blocked_reason=$(echo "$result" | grep -oP '(?<=<promise>BLOCKED:)[^<]+' || echo "unknown")
echo ""
echo "⚠️ Blocked: $blocked_reason"
echo "Press Enter to continue or Ctrl+C to stop"
read -r
fi
echo "--- End of iteration $i ---"
done
echo ""
echo "⚠️ Reached max iterations ($max_iterations)"
exit 1
PROMPT.md
# Context
- [PRD](link)
- [Development Guide](link)
- [Task Plan](plan.md)
- [Activity Log](activity.md)
# Instructions
1. **Orient**: Read activity.md for recent context, then read plan.md to find the highest priority task where `passes: false`.
2. **Understand**: Before coding, read any existing files you'll modify. Understand the current state.
3. **Implement**: Complete exactly ONE task. If blocked or unclear, document why in activity.md and move to next task.
4. **Verify**: Run relevant tests or validation before marking complete. Don't mark passes:true unless it actually works.
5. **Document**: Append a dated entry to activity.md:
-```
## YYYY-MM-DD HH:MM - Task X.X: [title]
- What changed
- Files modified
- Any issues or notes for next session
-```
6. **Commit**: One commit per task with format: `Task X.X: [description]`
# Rules
- ONE task per session
- Always use .venv for Python
- No git init/remote/push
- Target environment is Docker
- If stuck for 3+ attempts on same issue, mark task as blocked in plan.md and move on
# Exit conditions
- Task complete → end session normally
- All tasks `passes: true` → output `<promise>COMPLETE</promise>`
- Blocked on external dependency → output `<promise>BLOCKED: [reason]</promise>`
Conclusion
This is still a new approach, I’ve only had a few days with it but I’m liking it. For my current project it’s largely application based, but I think this combined with playwright mcp for headless browser control will make new web development projects more interesting too.