From 865cebe6ed162c410103ad39bd31add0830e79f2 Mon Sep 17 00:00:00 2001 From: skytow Date: Sun, 31 May 2026 03:10:40 -0400 Subject: [PATCH] Fix sed-substitution of {{TOPIC}} mangling/breaking on special chars start-loop.sh injected the user-provided topic into the plan-mode prompt templates via `sed "s|{{TOPIC}}|$TOPIC|g"`. In a sed replacement three characters are special: `&` (the whole matched text, i.e. `{{TOPIC}}`), the `|` delimiter, and `\`. So a topic like "Paywall & monetization" rendered as "Paywall {{TOPIC}} monetization", and a topic containing `|` crashed sed outright ("bad flag in substitute command"). Escape `\`, `&`, and `|` (and collapse newlines, since sed is line-oriented) into SED_SAFE_TOPIC before substitution. REVIEW_ID and MAX_ROUNDS are a generated id and an integer, so they need no escaping. Verified: a topic with `& | \` now round-trips verbatim; bash -n passes; doctor + smoke test (74/74) still green. stop-hook.sh is unaffected (it expands $TOPIC via bash, not sed). Co-Authored-By: Claude Opus 4.8 (1M context) --- plugins/claudex/scripts/start-loop.sh | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/plugins/claudex/scripts/start-loop.sh b/plugins/claudex/scripts/start-loop.sh index 862ba16..d8b5701 100755 --- a/plugins/claudex/scripts/start-loop.sh +++ b/plugins/claudex/scripts/start-loop.sh @@ -185,6 +185,14 @@ decision_signal: none" claudex_state_write "$STATE_FILE" "$STATE_CONTENT" || exit 3 claudex_lock_write "$LOCK_FILE" || exit 3 +# {{TOPIC}} is substituted into the prompt templates via sed below. The topic is +# user-provided and may contain characters that are special in a sed replacement: +# backslash, "&" (means "the whole matched text", i.e. "{{TOPIC}}"), and the "|" +# delimiter. Escape all three so the literal topic is inserted verbatim, and +# collapse newlines since sed operates line-by-line. (REVIEW_ID and MAX_ROUNDS are +# a generated id and an integer, so they need no escaping.) +SED_SAFE_TOPIC="$(printf '%s' "$TOPIC" | tr '\n\r' ' ' | sed -e 's/[\\&|]/\\&/g')" + # Print initial instructions to stdout. Claude will read these. case "$MODE" in plan) @@ -196,13 +204,13 @@ case "$MODE" in echo "Source: existing PLAN.md (--from-draft)" echo "" cat "$CLAUDE_PLUGIN_ROOT/scripts/prompts/plan-mode-from-draft.md" 2>/dev/null \ - | sed -e "s|{{TOPIC}}|$TOPIC|g" -e "s|{{REVIEW_ID}}|$REVIEW_ID|g" -e "s|{{MAX_ROUNDS}}|$MAX_ROUNDS|g" + | sed -e "s|{{TOPIC}}|$SED_SAFE_TOPIC|g" -e "s|{{REVIEW_ID}}|$REVIEW_ID|g" -e "s|{{MAX_ROUNDS}}|$MAX_ROUNDS|g" else echo "" echo "Round 1 - drafting plan." echo "" cat "$CLAUDE_PLUGIN_ROOT/scripts/prompts/plan-mode-init.md" 2>/dev/null \ - | sed -e "s|{{TOPIC}}|$TOPIC|g" -e "s|{{REVIEW_ID}}|$REVIEW_ID|g" + | sed -e "s|{{TOPIC}}|$SED_SAFE_TOPIC|g" -e "s|{{REVIEW_ID}}|$REVIEW_ID|g" fi ;; review)