[BUG] ralph-loop plugin fails when last assistant message is tool_use only (no text content)

Resolved 💬 3 comments Opened Feb 4, 2026 by godoppl Closed Mar 13, 2026

Preflight Checklist

  • [x] I have searched existing issues and this hasn't been reported yet
  • [x] This is a single bug report (please file separate reports for different bugs)
  • [x] I am using the latest version of Claude Code

What's Wrong?

The ralph-loop stop hook fails intermittently because it only checks the last assistant message for text type content. When the assistant's last message contains only tool_use or thinking blocks (no text), the hook outputs:

⚠️  Ralph loop: Assistant message contained no text content
   Ralph loop is stopping.

...and deletes the state file, breaking the loop unexpectedly.

Root Cause

In plugins/ralph-wiggum/hooks/stop-hook.sh lines 90-112:

LAST_OUTPUT=$(echo "$LAST_LINE" | jq -r '
  .message.content |
  map(select(.type == "text")) |
  map(.text) |
  join("\n")
' 2>&1)

# ...

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

The hook assumes the last assistant message always has text type content. However, when Claude does tool calls, the message often contains only tool_use blocks without any text block.

What Should Happen?

The hook should:

  1. Search backwards through recent assistant messages to find one with text content, OR
  2. Recognize tool_use as valid activity (assistant is still working) and continue the loop

Suggested Fix

Replace lines 80-112 with:

# FIX: Search backwards through recent assistant messages to find one with text content
# The last message might be tool_use only, so we need to look further back
LAST_OUTPUT=""
MESSAGES_TO_CHECK=10  # Check last 10 assistant messages

# Get last N assistant messages and search for text content
while IFS= read -r line; do
  # Try to extract text content from this message
  TEXT_CONTENT=$(echo "$line" | jq -r '
    .message.content // [] |
    map(select(.type == "text")) |
    map(.text) |
    join("\n")
  ' 2>/dev/null || echo "")

  if [[ -n "$TEXT_CONTENT" ]]; then
    LAST_OUTPUT="$TEXT_CONTENT"
    break
  fi
done < <(grep '"role":"assistant"' "$TRANSCRIPT_PATH" | tail -"$MESSAGES_TO_CHECK" | tac)

# If still no text found, check if there's at least tool_use activity (assistant is active)
if [[ -z "$LAST_OUTPUT" ]]; then
  # Check if last message has tool_use - this means assistant is actively working
  LAST_LINE=$(grep '"role":"assistant"' "$TRANSCRIPT_PATH" | tail -1)
  HAS_TOOL_USE=$(echo "$LAST_LINE" | jq -r '
    .message.content // [] |
    map(select(.type == "tool_use")) |
    length
  ' 2>/dev/null || echo "0")

  if [[ "$HAS_TOOL_USE" -gt 0 ]]; then
    # Assistant is doing tool calls - continue the loop without needing text
    LAST_OUTPUT="[Assistant is executing tools]"
  else
    echo "⚠️  Ralph loop: No text content found in recent assistant messages" >&2
    echo "   Checked last $MESSAGES_TO_CHECK messages" >&2
    echo "   Ralph loop is stopping." >&2
    rm "$RALPH_STATE_FILE"
    exit 0
  fi
fi

Steps to Reproduce

  1. Install ralph-loop plugin
  2. Start a ralph loop: /ralph-loop "Do some task" --max-iterations 5
  3. During iteration, do multiple tool calls without text output between them
  4. Exit (loop should continue)
  5. Observe: Loop breaks with "Assistant message contained no text content"

Environment

  • Claude Code version: 2.1.31
  • Platform: Linux
  • The bug is in the plugin code itself, so it affects all platforms

Additional Context

This bug causes ralph-loop to appear "unreliable" or "intermittent" - it works when the last message has text, fails when it's tool_use only. The fix above has been tested and successfully runs 5+ iterations without breaking.

View original on GitHub ↗

This issue has 3 comments on GitHub. Read the full discussion on GitHub ↗