diff --git a/ralph-loop/hooks/capture-response.sh b/ralph-loop/hooks/capture-response.sh
index d8da50a2a..b0f424875 100755
--- a/ralph-loop/hooks/capture-response.sh
+++ b/ralph-loop/hooks/capture-response.sh
@@ -20,8 +20,10 @@ if [[ ! -f "$STATE_FILE" ]]; then
exit 0
fi
-# Extract completion promise from state file frontmatter
-FRONTMATTER=$(sed -n '/^---$/,/^---$/{ /^---$/d; p; }' "$STATE_FILE")
+# Extract completion promise from the state file frontmatter (the block
+# between the opening `---` on line 1 and the first closing `---`; a `---`
+# rule in the prompt body must not reopen the range).
+FRONTMATTER=$(awk 'NR == 1 && /^---$/ { in_fm = 1; next } in_fm && /^---$/ { exit } in_fm' "$STATE_FILE")
COMPLETION_PROMISE=$(echo "$FRONTMATTER" | grep '^completion_promise:' | sed 's/completion_promise: *//' | sed 's/^"\(.*\)"$/\1/')
# No promise configured, nothing to check
diff --git a/ralph-loop/hooks/stop-hook.sh b/ralph-loop/hooks/stop-hook.sh
index ba0ba3434..d37aea9b1 100755
--- a/ralph-loop/hooks/stop-hook.sh
+++ b/ralph-loop/hooks/stop-hook.sh
@@ -21,8 +21,10 @@ if [[ ! -f "$STATE_FILE" ]]; then
exit 0
fi
-# Parse state file frontmatter
-FRONTMATTER=$(sed -n '/^---$/,/^---$/{ /^---$/d; p; }' "$STATE_FILE")
+# Parse state file frontmatter: the block between the opening `---` on line 1
+# and the first closing `---`. A `---` later in the prompt body is a Markdown
+# rule, not a second frontmatter block, so it must not reopen the range.
+FRONTMATTER=$(awk 'NR == 1 && /^---$/ { in_fm = 1; next } in_fm && /^---$/ { exit } in_fm' "$STATE_FILE")
ITERATION=$(echo "$FRONTMATTER" | grep '^iteration:' | sed 's/iteration: *//')
MAX_ITERATIONS=$(echo "$FRONTMATTER" | grep '^max_iterations:' | sed 's/max_iterations: *//')
COMPLETION_PROMISE=$(echo "$FRONTMATTER" | grep '^completion_promise:' | sed 's/completion_promise: *//' | sed 's/^"\(.*\)"$/\1/')
@@ -54,8 +56,9 @@ if [[ $MAX_ITERATIONS -gt 0 ]] && [[ $ITERATION -ge $MAX_ITERATIONS ]]; then
exit 0
fi
-# Extract prompt text (everything after the closing --- in frontmatter)
-PROMPT_TEXT=$(awk '/^---$/{i++; next} i>=2' "$STATE_FILE")
+# Extract prompt text: everything after the closing --- of the frontmatter,
+# verbatim, including any --- rules the prompt itself contains.
+PROMPT_TEXT=$(awk 'in_body { print; next } NR == 1 && /^---$/ { in_fm = 1; next } in_fm && /^---$/ { in_body = 1 }' "$STATE_FILE")
if [[ -z "$PROMPT_TEXT" ]]; then
echo "Ralph loop: no prompt text found in state file. Stopping." >&2
@@ -66,7 +69,14 @@ fi
# Increment iteration
NEXT_ITERATION=$((ITERATION + 1))
TEMP_FILE="${STATE_FILE}.tmp.$$"
-sed "s/^iteration: .*/iteration: $NEXT_ITERATION/" "$STATE_FILE" > "$TEMP_FILE"
+# Only the frontmatter line is rewritten; an `iteration:` line in the prompt
+# body is prose and stays as it is.
+awk -v next_iteration="$NEXT_ITERATION" '
+ NR == 1 && /^---$/ { in_fm = 1; print; next }
+ in_fm && /^---$/ { in_fm = 0 }
+ in_fm && /^iteration:/ { print "iteration: " next_iteration; next }
+ { print }
+' "$STATE_FILE" > "$TEMP_FILE"
mv "$TEMP_FILE" "$STATE_FILE"
# Build the followup message: iteration context + original prompt
diff --git a/ralph-loop/tests/body-separators.test.sh b/ralph-loop/tests/body-separators.test.sh
new file mode 100755
index 000000000..a4ba2f3ff
--- /dev/null
+++ b/ralph-loop/tests/body-separators.test.sh
@@ -0,0 +1,101 @@
+#!/bin/bash
+
+# Tests for prompts that contain `---` lines (Markdown rules) in the body of
+# .cursor/ralph/scratchpad.md.
+# Standalone (no test runner): bash ralph-loop/tests/body-separators.test.sh
+#
+# The hooks must read the frontmatter as the block between the opening `---`
+# on line 1 and the first closing `---` only, feed the prompt body back
+# verbatim, and rewrite the `iteration:` line in the frontmatter alone.
+
+set -uo pipefail
+
+HOOKS_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)/hooks"
+FAILURES=0
+
+check() {
+ local name="$1" ok="$2"
+ if [[ "$ok" == "1" ]]; then
+ echo "PASS: $name"
+ else
+ echo "FAIL: $name"
+ FAILURES=$((FAILURES + 1))
+ fi
+}
+
+make_project() {
+ local dir
+ dir="$(mktemp -d)"
+ mkdir -p "$dir/.cursor/ralph"
+ cat > "$dir/.cursor/ralph/scratchpad.md" <<'EOF'
+---
+iteration: 1
+max_iterations: 5
+completion_promise: "ALL GREEN"
+---
+
+# Spec
+
+---
+
+Step A: iteration: keep this line as it is.
+
+---
+
+Step B.
+EOF
+ echo "$dir"
+}
+
+# --- stop hook: the followup carries the body verbatim, rules included
+
+project="$(make_project)"
+followup="$(echo '{"status":"completed","loop_count":0}' | CURSOR_PROJECT_DIR="$project" bash "$HOOKS_DIR/stop-hook.sh" 2>/tmp/ralph-stderr.$$ | jq -r '.followup_message')"
+stderr="$(cat /tmp/ralph-stderr.$$)"
+rm -f /tmp/ralph-stderr.$$
+
+expected_body="$(awk 'in_body { print; next } NR == 1 && /^---$/ { in_fm = 1; next } in_fm && /^---$/ { in_body = 1 }' "$project/.cursor/ralph/scratchpad.md" | sed 's/^iteration: .*//')"
+check "stop hook does not report a corrupted state file" "$([[ -z "$stderr" ]] && echo 1 || echo 0)"
+check "stop hook emits a followup" "$([[ -n "$followup" ]] && echo 1 || echo 0)"
+check "followup header names iteration 2 and the promise" "$([[ "$followup" == "[Ralph loop iteration 2. To complete: output ALL GREEN ONLY when genuinely true.]"* ]] && echo 1 || echo 0)"
+check "followup keeps both --- rules" "$([[ "$(printf '%s\n' "$followup" | grep -c '^---$')" == "2" ]] && echo 1 || echo 0)"
+check "followup keeps the body's iteration: line" "$(printf '%s\n' "$followup" | grep -q '^Step A: iteration: keep this line as it is\.$' && echo 1 || echo 0)"
+check "followup ends with Step B" "$([[ "$followup" == *"Step B." ]] && echo 1 || echo 0)"
+
+# --- stop hook: only the frontmatter iteration is rewritten
+
+state="$project/.cursor/ralph/scratchpad.md"
+check "frontmatter iteration advanced to 2" "$([[ "$(sed -n '2p' "$state")" == "iteration: 2" ]] && echo 1 || echo 0)"
+check "body iteration: line untouched" "$(grep -q '^Step A: iteration: keep this line as it is\.$' "$state" && echo 1 || echo 0)"
+check "state file still has exactly one frontmatter iteration line" "$([[ "$(grep -c '^iteration:' "$state")" == "1" ]] && echo 1 || echo 0)"
+rm -rf "$project"
+
+# --- capture hook: the promise is still read from the frontmatter
+
+project="$(make_project)"
+echo '{"text":"done. ALL GREEN"}' | CURSOR_PROJECT_DIR="$project" bash "$HOOKS_DIR/capture-response.sh"
+check "capture hook sets the done flag with rules in the body" "$([[ -f "$project/.cursor/ralph/done" ]] && echo 1 || echo 0)"
+rm -rf "$project"
+
+# --- body with a second frontmatter-like block does not corrupt the loop
+
+project="$(make_project)"
+cat >> "$project/.cursor/ralph/scratchpad.md" <<'EOF'
+
+Write a SKILL.md that starts with:
+
+---
+name: example
+iteration: not a number
+---
+EOF
+followup="$(echo '{"status":"completed","loop_count":0}' | CURSOR_PROJECT_DIR="$project" bash "$HOOKS_DIR/stop-hook.sh" 2>/dev/null | jq -r '.followup_message')"
+check "a frontmatter-like block in the body does not stop the loop" "$([[ -f "$project/.cursor/ralph/scratchpad.md" && -n "$followup" ]] && echo 1 || echo 0)"
+check "the quoted block comes back verbatim" "$(printf '%s\n' "$followup" | grep -q '^iteration: not a number$' && echo 1 || echo 0)"
+rm -rf "$project"
+
+if [[ "$FAILURES" -gt 0 ]]; then
+ echo "$FAILURES failure(s)"
+ exit 1
+fi
+echo "all passed"