Problem
Both brain paths trust their output. pipeline.brain returns result.stdout.strip() from claude -p unbounded; brain_via_bridge returns the polled payload from _last_delivery as-is. A very long reply, control characters, or markdown will be handed straight to Piper, and an empty reply produces silent or broken speech.
Why it matters
The reply is spoken aloud, so unbounded or malformed text becomes a broken or endless utterance. The transcript is also untrusted spoken input driving the prompt.
Approach
Add a sanitize_reply() applied to both brain paths before speak: cap length, strip control characters and markdown that the VOICE_SYSTEM_PROMPT already asks the model to avoid, and on an empty reply substitute a short spoken fallback. Confirm the existing ssh_cli_send shell-quoting of the untrusted prompt stays intact (it uses shlex.quote). Keep the spoken error strings already returned on timeout/transport failure.
Acceptance criteria
Problem
Both brain paths trust their output.
pipeline.brainreturnsresult.stdout.strip()fromclaude -punbounded;brain_via_bridgereturns the polledpayloadfrom_last_deliveryas-is. A very long reply, control characters, or markdown will be handed straight to Piper, and an empty reply produces silent or broken speech.Why it matters
The reply is spoken aloud, so unbounded or malformed text becomes a broken or endless utterance. The transcript is also untrusted spoken input driving the prompt.
Approach
Add a
sanitize_reply()applied to both brain paths beforespeak: cap length, strip control characters and markdown that theVOICE_SYSTEM_PROMPTalready asks the model to avoid, and on an empty reply substitute a short spoken fallback. Confirm the existingssh_cli_sendshell-quoting of the untrusted prompt stays intact (it usesshlex.quote). Keep the spoken error strings already returned on timeout/transport failure.Acceptance criteria
pipeline.brainandbrain_via_bridgeoutputs pass through the sanitizer