#!/usr/bin/env bash # send-message.sh — reliably deliver a message to a tmux pane running an # interactive REPL (e.g. a Claude Code / Codex agent). # # WHY THIS EXISTS # Pasting multi-line text into an interactive agent REPL via `tmux send-keys` # is unreliable: the text lands in the input box but a single trailing Enter # in the same keystroke stream is frequently swallowed, so the message sits as # an UNSUBMITTED DRAFT ("Press up to edit queued messages") and the agent never # sees it. The mechanical fix is: paste as a bracketed paste (so embedded # newlines don't submit early), pause, then send Enter as its OWN keystroke, # pause, and send Enter again to flush. An extra Enter on an empty prompt is a # no-op in Claude Code, so the double-Enter is safe. # # USAGE # send-message.sh [-L socket_name] -t -m "message" # send-message.sh [-L socket_name] -t -f # echo "message" | send-message.sh [-L socket_name] -t # ssh host bash -s -- -L socket -t -b "$(base64 -w0 <<lead stall. # 3 usage error set -uo pipefail SOCKET_NAME=""; TARGET=""; MSG=""; FILE=""; B64=""; RETRIES=2; VERBOSE=0 usage() { sed -n '2,34p' "$0"; exit "${1:-3}"; } while getopts "L:t:m:f:b:r:vh" o; do case "$o" in L) SOCKET_NAME=$OPTARG ;; t) TARGET=$OPTARG ;; m) MSG=$OPTARG ;; f) FILE=$OPTARG ;; b) B64=$OPTARG ;; r) RETRIES=$OPTARG ;; v) VERBOSE=1 ;; h) usage 0 ;; *) usage 3 ;; esac done [ -n "$TARGET" ] || { echo "ERROR: -t TARGET is required" >&2; usage 3; } if [ -n "$B64" ]; then MSG=$(printf '%s' "$B64" | base64 -d) || { echo "ERROR: bad -b base64" >&2; exit 3; } elif [ -n "$FILE" ]; then [ -r "$FILE" ] || { echo "ERROR: cannot read $FILE" >&2; exit 3; }; MSG=$(cat -- "$FILE") elif [ -z "$MSG" ] && [ ! -t 0 ]; then MSG=$(cat) fi [ -n "$MSG" ] || { echo "ERROR: empty message (use -m, -f, or stdin)" >&2; exit 3; } tmux_cmd=(tmux) if [ -n "$SOCKET_NAME" ]; then tmux_cmd+=(-L "$SOCKET_NAME") fi # tmux accepts `=session` for some commands, but pane-level commands such as # capture-pane require a pane-qualified target. Keep exact-session addressing # convenient while avoiding accidental prefix matches. EFFECTIVE_TARGET=$TARGET if [[ "$TARGET" == =* && "$TARGET" != *:* ]]; then EFFECTIVE_TARGET="${TARGET}:0.0" fi # Target must resolve to a live pane. if ! "${tmux_cmd[@]}" list-panes -t "$EFFECTIVE_TARGET" >/dev/null 2>&1; then echo "ERROR: tmux target not found: $TARGET" >&2; exit 1 fi QUEUED_RE='Press up to edit queued messages' # A distinctive tail of the message to spot an unsubmitted draft on the input line. snippet=$(printf '%s' "$MSG" | tr '\n' ' ' | tr -s ' ' | sed 's/[^[:print:]]//g' | tail -c 32) # 1) Paste the body as a bracketed paste so multi-line content does not submit # line-by-line. load-buffer/paste-buffer is far safer than `send-keys -l`. # Buffer name MUST be unique per invocation: concurrent senders on the shared # tmux server race a fixed name (load overwrites load, -d deletes underneath), # cross-delivering or dropping messages — bit the fleet on the 2026-07-09 # simultaneous restart (briefs swapped between sessions). BUF="__mosaic_send_$$_$(date +%s%N)" printf '%s' "$MSG" | "${tmux_cmd[@]}" load-buffer -b "$BUF" - # -p = bracketed paste when the client supports it; fall back if not. "${tmux_cmd[@]}" paste-buffer -d -p -b "$BUF" -t "$EFFECTIVE_TARGET" 2>/dev/null \ || "${tmux_cmd[@]}" paste-buffer -d -b "$BUF" -t "$EFFECTIVE_TARGET" \ || "${tmux_cmd[@]}" delete-buffer -b "$BUF" 2>/dev/null # ^ -d deletes the buffer only on a SUCCESSFUL paste; if both attempts fail # (e.g. the target vanished since the liveness check), delete explicitly — # named buffers are exempt from tmux's buffer-limit eviction, so orphans # would otherwise accumulate forever. sleep 0.5 # 2) Submit, then POSITIVELY confirm submission by DRAFT TRANSITION, not by prompt # glyph. The historical bug was treating ABSENCE of a draft as delivery; the # 2026-08 fix over-corrected to glyph inference (grep '❯|^>|│ >'), which locates # only Claude Code's box and false-NEGATIVES every glyphless REPL (pi renders a # U+2500 rule, no glyph) — a delivered message reported "UNDELIVERED", driving a # retry that duplicates it. Runtime-agnostic evidence: our message tail sits on # the INPUT line (located by the cursor row, not a glyph) BEFORE Enter, and has # LEFT it AFTER — that transition is positive proof of submission and needs no # glyph. Absence alone still never means delivered: if we never saw our draft on # the input line we stay UNCONFIRMED (wrong/dead pane), and a draft that never # leaves the input line stays a DRAFT (exit 2), preserving both historical guards. _cursor_line() { # echo the pane's current input (cursor) line, glyph-free local cy line cy=$("${tmux_cmd[@]}" display-message -p -t "$EFFECTIVE_TARGET" -F '#{cursor_y}' 2>/dev/null) || return 1 [ -n "$cy" ] || return 1 "${tmux_cmd[@]}" capture-pane -t "$EFFECTIVE_TARGET" -p 2>/dev/null | sed -n "$((cy + 1))p" } _draft_on_input() { # true iff our message tail is sitting on the input line now [ -n "$snippet" ] || return 1 printf '%s' "$(_cursor_line)" | grep -qF "$snippet" } # Baseline: after the paste, our draft must be on the input line. This is positive # proof we are on the right pane and the paste landed — the anchor the transition # check measures against. saw_draft=0 _draft_on_input && saw_draft=1 status="unconfirmed" for attempt in $(seq 1 $((RETRIES + 1))); do "${tmux_cmd[@]}" send-keys -t "$EFFECTIVE_TARGET" Enter sleep 1.2 pane=$("${tmux_cmd[@]}" capture-pane -t "$EFFECTIVE_TARGET" -p 2>/dev/null) if printf '%s' "$pane" | grep -qF "$QUEUED_RE"; then status="queued"; break fi # POSITIVE draft evidence from a located prompt box, when one exists. This is the # cursor-row check's blind spot: a pane in COOKED mode (a plain shell whose # foreground process never reads stdin) echoes our paste via the kernel line # discipline and moves the cursor off it on Enter, which is indistinguishable from # a real submit by cursor row alone. If a prompt box IS locatable and still carries # our tail, that is affirmative proof the message was not consumed. Absence of a # glyph is still never used for anything — that inference is the original E7 bug. promptline=$(printf '%s' "$pane" | grep -E '❯|^>|│ >' | tail -1) if [ -n "$promptline" ] && [ -n "$snippet" ] && printf '%s' "$promptline" | grep -qF "$snippet"; then status="draft"; continue fi if [ "$saw_draft" = 1 ]; then if _draft_on_input; then status="draft"; continue # still on the input line => not submitted; flush + retry fi status="delivered"; break # left the input line => positively submitted fi # No confirmed baseline yet: try to (re)acquire it; never infer delivery from absence. if _draft_on_input; then saw_draft=1; status="draft"; continue; fi status="unconfirmed"; continue done [ "$VERBOSE" = 1 ] && { echo "--- pane tail ($TARGET) ---"; printf '%s\n' "$pane" | tail -4; echo "---"; } case "$status" in delivered) echo "✓ delivered to $TARGET"; exit 0 ;; queued) echo "✓ queued to $TARGET (agent busy — will process when it returns to prompt)"; exit 0 ;; draft) echo "✗ still an unsubmitted draft on $TARGET after $RETRIES flush attempts" >&2; exit 2 ;; unconfirmed) echo "✗ could not confirm submission on $TARGET: REPL input prompt not locatable after $((RETRIES + 1)) attempts — message may be UNDELIVERED (check target/pane, retry, or escalate)" >&2; exit 2 ;; *) echo "✗ could not confirm submission on $TARGET (unexpected state '$status')" >&2; exit 2 ;; esac