Skip to content

ralph-loop: 3 bugs in stop-hook.sh and setup-ralph-loop.sh (promise detection, awk prompt extraction, YAML quoting) #449

@georgiedekker

Description

@georgiedekker

Bug Report

Three independent bugs found in the ralph-loop plugin. Fixes are minimal (6 lines changed across 2 files). PR #448 was auto-declined due to external contributor policy, so documenting here with full fixes.


Bug 1: Promise detection matches without <promise> tags

File: hooks/stop-hook.sh, line 119

Problem: The perl command uses -pe which prints input unchanged when no substitution matches. If Claude's entire output happens to equal the completion promise text (without using <promise> tags), the loop incorrectly exits.

# Current (broken): -pe prints full input when no <promise> tags found
PROMISE_TEXT=$(echo "$LAST_OUTPUT" | perl -0777 -pe 's/.*?<promise>(.*?)<\/promise>.*/$1/s; ...')

Fix: Change -pe to -ne with conditional print — only outputs when <promise> tags are actually present:

PROMISE_TEXT=$(echo "$LAST_OUTPUT" | perl -0777 -ne 'if (/<promise>(.*?)<\/promise>/s) { $t = $1; $t =~ s/^\s+|\s+$//g; $t =~ s/\s+/ /g; print $t; }' 2>/dev/null || echo "")

Bug 2: awk silently drops --- lines from user prompts

File: hooks/stop-hook.sh, line 136

Problem: The awk rule /^---$/{i++; next} matches ALL --- lines in the file, not just the two YAML frontmatter delimiters. Any --- line in the user's prompt body is silently consumed and removed from the prompt fed back to Claude.

The comment on line 135 says "Use i>=2 instead of i==2 to handle --- in prompt content" — but i>=2 only fixes printing of lines between --- markers. The --- line itself is still eaten by the {i++; next} action.

# Current (broken): ALL --- lines are consumed
PROMPT_TEXT=$(awk '/^---$/{i++; next} i>=2' "$RALPH_STATE_FILE")

# Reproduction: prompt containing markdown separator
# ---
# active: true
# iteration: 1
# ---
#
# Refactor this config:
# ---          ← THIS LINE IS SILENTLY REMOVED
# key: value
# ---          ← THIS LINE IS SILENTLY REMOVED

Fix: Add && i<2 guard so only the first two frontmatter delimiters are consumed:

PROMPT_TEXT=$(awk '/^---$/ && i<2 {i++; next} i>=2' "$RALPH_STATE_FILE")

Bug 3: YAML quoting breaks with double quotes in completion promise

File: scripts/setup-ralph-loop.sh, lines 134-138 (write side) + hooks/stop-hook.sh, line 25 (read side)

Problem: The completion promise is wrapped in double quotes without escaping internal quotes, producing invalid YAML:

# Current (broken):
COMPLETION_PROMISE_YAML="\"$COMPLETION_PROMISE\""

# With --completion-promise 'say "hello"':
# Produces: completion_promise: "say "hello""  ← invalid YAML

Fix (write side, setup-ralph-loop.sh): Escape backslashes and double quotes:

ESCAPED=$(echo "$COMPLETION_PROMISE" | sed 's/\\/\\\\/g; s/"/\\"/g')
COMPLETION_PROMISE_YAML="\"$ESCAPED\""

Fix (read side, stop-hook.sh line 25): Add unescaping:

COMPLETION_PROMISE=$(echo "$FRONTMATTER" | grep '^completion_promise:' | sed 's/completion_promise: *//' | sed 's/^"\(.*\)"$/\1/' | sed 's/\\"/"/g; s/\\\\/\\/g')

Full diff

diff --git a/plugins/ralph-loop/hooks/stop-hook.sh b/plugins/ralph-loop/hooks/stop-hook.sh
index abc1234..def5678 100755
--- a/plugins/ralph-loop/hooks/stop-hook.sh
+++ b/plugins/ralph-loop/hooks/stop-hook.sh
@@ -22,7 +22,7 @@ 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/')
+COMPLETION_PROMISE=$(echo "$FRONTMATTER" | grep '^completion_promise:' | sed 's/completion_promise: *//' | sed 's/^"\(.*\)"$/\1/' | sed 's/\\"/"/g; s/\\\\/\\/g')

@@ -116,8 +116,8 @@ 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 "")
+  # -ne only prints when match found (unlike -pe which prints input unchanged on no match)
+  PROMISE_TEXT=$(echo "$LAST_OUTPUT" | perl -0777 -ne 'if (/<promise>(.*?)<\/promise>/s) { $t = $1; $t =~ s/^\s+|\s+$//g; $t =~ s/\s+/ /g; print $t; }' 2>/dev/null || echo "")

@@ -133,7 +133,7 @@ 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")
+PROMPT_TEXT=$(awk '/^---$/ && i<2 {i++; next} i>=2' "$RALPH_STATE_FILE")

diff --git a/plugins/ralph-loop/scripts/setup-ralph-loop.sh b/plugins/ralph-loop/scripts/setup-ralph-loop.sh
--- a/plugins/ralph-loop/scripts/setup-ralph-loop.sh
+++ b/plugins/ralph-loop/scripts/setup-ralph-loop.sh
@@ -134,7 +134,8 @@ if [[ -n "$COMPLETION_PROMISE" ]] && [[ "$COMPLETION_PROMISE" != "null" ]]; then
-  COMPLETION_PROMISE_YAML="\"$COMPLETION_PROMISE\""
+  ESCAPED=$(echo "$COMPLETION_PROMISE" | sed 's/\\/\\\\/g; s/"/\\"/g')
+  COMPLETION_PROMISE_YAML="\"$ESCAPED\""

Environment

  • macOS 15.3 (Darwin 25.2.0)
  • bash 3.2 (macOS default)
  • Claude Code latest
  • Plugin installed from marketplace 2026-02-21

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions