ralph-wiggum stop hook: jq error handler is unreachable under set -e, leaving a stale ralph-loop state file
What's Wrong?
In plugins/ralph-wiggum/hooks/stop-hook.sh, the jq parse-failure handler is unreachable.
The script runs under set -euo pipefail (line 7). Line 90 assigns from a jq command substitution, and lines 97–105 are meant to handle a parse failure:
LAST_OUTPUT=$(echo "$LAST_LINE" | jq -r '...' 2>&1)
# Check if jq succeeded
if [[ $? -ne 0 ]]; then
echo "⚠️ Ralph loop: Failed to parse assistant message JSON" >&2
...
rm "$RALPH_STATE_FILE"
exit 0
fi
Under set -e, a failing command substitution in an assignment aborts the script at line 90, so the if block never executes.
The consequence that bites is the leftover state file. When a transcript line matches "role":"assistant" but is not valid JSON (for example, a JSONL line truncated mid-write):
| | intended | actual |
|---|---|---|
| exit code | 0 | 5 (jq's status) |
| user guidance | printed | never printed |
| .claude/ralph-loop.local.md | removed | left on disk |
A stale state file makes the next session's Stop hook see an active loop and resume it with the old prompt — exactly what the handler was written to prevent.
What Should Happen?
On a jq parse failure the hook should do what the handler says: print the guidance, remove .claude/ralph-loop.local.md, and exit 0 so the loop stops cleanly.
Error Messages/Logs
Nothing is printed. The hook exits with jq's status instead:
exit=5
state file still present? YES
Expected (and what happens once the handler is reachable):
⚠️ Ralph loop: Failed to parse assistant message JSON
Error: jq: error (at <stdin>:1): ...
This may indicate a transcript format issue
Ralph loop is stopping.
exit=0
state file still present? no
Steps to Reproduce
Runs against the real script, no plugin install needed:
#!/bin/bash
# usage: repro.sh <path to plugins/ralph-wiggum/hooks/stop-hook.sh>
HOOK="$(cd "$(dirname "$1")" && pwd)/$(basename "$1")"
SANDBOX="$(mktemp -d)"; cd "$SANDBOX" || exit 1; mkdir -p .claude
cat > .claude/ralph-loop.local.md <<'STATE'
---
iteration: 1
max_iterations: 10
completion_promise: "DONE"
---
Keep improving the test suite.
STATE
# a line that matches '"role":"assistant"' but is not valid JSON
printf '%s\n' '{"role":"assistant","message":{"content":[{"type":"text","text":"hi"' > broken.jsonl
printf '{"transcript_path": "%s", "stop_hook_active": false}' "$SANDBOX/broken.jsonl" \
| bash "$HOOK"; echo "exit=$?"
echo "state file still present? $([ -f .claude/ralph-loop.local.md ] && echo YES || echo no)"
Observed on main (commit of 2026-09-17): exit=5, no guidance printed, state file still present? YES.
Suggested fix
Wrap the assignment in set +e / set -e and capture the status explicitly. This is the idiom the repo already uses in plugins/plugin-dev/skills/hook-development/scripts/test-hook.sh#L189-L192:
# Parse JSON with error handling
+set +e
LAST_OUTPUT=$(echo "$LAST_LINE" | jq -r '
.message.content |
map(select(.type == "text")) |
map(.text) |
join("\n")
' 2>&1)
+JQ_STATUS=$?
+set -e
# Check if jq succeeded
-if [[ $? -ne 0 ]]; then
+if [[ $JQ_STATUS -ne 0 ]]; then
One file, 8 insertions, 1 deletion, bash -n clean.
I verified the patch with the corrupted transcript above plus two healthy-path controls, so a green result cannot come from the hook simply doing nothing:
| case | before | after |
|---|---|---|
| corrupted transcript | exit 5, no message, state file remains | exit 0, message shown, state file removed |
| control — healthy transcript | decision=block, iteration 1→2 | unchanged |
| control — completion promise | loop ends, state file removed | unchanged |
Both controls pass before and after, so the change is confined to the error path.
Branch with the commit, in case it is useful: https://github.com/2001056/claude-code/tree/fix/ralph-stop-hook-jq-error-handler
I tried to open this as a PR first, but CreatePullRequest is refused for outside forks, so filing it here instead. Happy to send a PR if that channel is reopened.
Is this a regression?
Not sure / haven't tested previous versions
This issue has 1 comment on GitHub. Read the full discussion on GitHub ↗