Claude Code ignores Enter from tmux send-keys: causes and fix
On this page
What it looks likeCause 1: no Enter was sentCause 2: text and Enter in one burstCause 3: Claude Code 2.1.277 and later ignore an early EnterHow to tell whether a prompt was submittedA submit script that retries Enter safelyIf you only need automation, skip the TUIHow Codeman handles itIf a prompt you send to Claude Code with tmux send-keys just sits in the input box, the text arrived but the Enter did not count. Send Enter as its own keypress, then read the pane with tmux capture-pane and press Enter again for as long as your prompt is still sitting in the composer.
What it looks like
Your script runs something like tmux send-keys -t work "fix the failing test" Enter. The pane shows your text on the ❯ input line, the spinner never starts, and whatever you have waiting for the turn to finish times out on a turn that never began.
There are three separate causes, and a script that drives Claude Code has to handle all of them.
Cause 1: no Enter was sent
tmux send-keys with -l treats every argument as literal text. The tmux manual says -l "disables key name lookup and processes the keys as literal UTF-8 characters", so this types the word Enter into the prompt instead of pressing it:
tmux send-keys -t work -l "fix the failing test" Enter # types "...testEnter"
Use two calls: one with -l for the text, one without it for the key.
Cause 2: text and Enter in one burst
Claude Code's interface is built on Ink. Codeman's tmux layer sends the text and the Enter as two separate tmux commands with a short pause between them, and its source records why: when both went out in a single tmux invocation, Ink treated the Enter as a newline inside the input rather than as a submit. A 50 to 100 ms gap between the two calls avoids it.
Cause 3: Claude Code 2.1.277 and later ignore an early Enter
Starting with Claude Code 2.1.277, Codeman's maintainers measured a new behaviour: the CLI accepts typed text as soon as its input box is drawn, but ignores Enter for roughly the first 30 to 50 seconds after that. In their measurement an Enter sent 28 seconds after the composer appeared left the prompt stranded, and one sent at 51 seconds submitted it. This is observed behaviour, not something Anthropic documents, so later versions may change it.
The practical effect: any script that starts claude and sends a prompt as soon as it sees the ❯ glyph will hit it every time, and so will a script that sends one Enter and then waits.
One more trap on first launch: in a directory Claude Code has not seen before, it shows a workspace trust dialog. Text you send while that dialog is on screen goes to the dialog, not the composer. Answer it (by hand, or by reading the screen) before sending anything.
How to tell whether a prompt was submitted
Read the screen, not your own log. The composer is the last line that starts with the ❯ glyph; earlier ❯ lines are previous prompts echoed into the transcript. Once a prompt is taken, the last composer line is empty.
tmux capture-pane -p -t work | grep -E '^[[:space:]]*❯' | tail -n 1
Two details matter when you compare that line with what you sent. Claude Code draws a no-break space after the glyph, which [:space:] does not reliably match, and a long prompt wraps onto indented continuation lines. Comparing only the first couple of dozen characters with all whitespace removed avoids both problems.
A submit script that retries Enter safely
This script sends the text, sends Enter, and then checks the composer on a schedule that reaches 60 seconds. It presses Enter again only while the composer still holds the start of your prompt.
#!/usr/bin/env bash
# submit.sh <tmux-target> <prompt>: type a one-line prompt into Claude Code
# and keep pressing Enter until it has actually left the composer.
set -u
target=$1 prompt=$2
glyph='❯'
nbsp=$(printf '\302\240')
tmux send-keys -t "$target" -l -- "$prompt"
sleep 0.1
tmux send-keys -t "$target" Enter
# First 24 non-space characters of the prompt, for comparison.
want=$(printf '%s' "$prompt" | tr -d '[:space:]')
want=${want:0:24}
for delay in 2 3 5 5 5 10 10 10 10; do
sleep "$delay"
line=$(tmux capture-pane -p -t "$target" | grep -E "^[[:space:]]*$glyph" | tail -n 1)
if [ -z "$line" ]; then
echo "no composer on screen; not guessing" >&2; exit 2
fi
have=$(printf '%s' "${line#*"$glyph"}" | tr -d '[:space:]' | sed "s/$nbsp//g")
case "$have" in
"") echo "submitted"; exit 0 ;; # composer empty: prompt taken
"$want"*) tmux send-keys -t "$target" Enter ;; # still there: press Enter again
*) echo "composer holds other text; stopping" >&2; exit 3 ;;
esac
done
echo "still unsubmitted after about 60s" >&2; exit 1
./submit.sh work "run the test suite and fix what fails"
The stop conditions are the important part:
- Empty composer: the prompt was taken. Stop, so you never nudge a turn that is already running.
- Other text in the composer: someone (you, or the CLI) typed there. Pressing Enter would submit their text, not yours.
- No composer line at all: do nothing. The
❯glyph is also the default prompt of popular shell themes such as starship, so a script that presses Enter blindly into the wrong pane can accept ay/Ndefault or feed a running program.
Keep prompts to a single line. For long instructions, write them to a file and send a short prompt telling Claude to read it.
If you only need automation, skip the TUI
If nobody needs to attach to the session later, claude -p "your prompt" runs Claude Code non-interactively, with no input box to drive. Anthropic documents it for CI, pre-commit hooks and batch jobs in the non-interactive mode docs. The tmux approach is worth its complexity when you want an interactive session that keeps running and that a person can attach to.
How Codeman handles it
Codeman runs each agent CLI in its own tmux session and exposes an HTTP API for sending input, so it runs into the same problems and handles them on the server:
- Text and Enter always go out as separate tmux commands.
- API input has one rule: the payload must end with
\r, or no Enter is sent. The request still succeeds and the text sits in the composer. Embedded newlines are stripped, not rejected. - Since Codeman 1.31.0, every programmatic write through tmux that carried a
\ris verified. The server reads the pane on a 2 to 60 second schedule and re-sends Enter only while the last composer line still holds the start of what it sent. An empty composer, other text, or no composer line ends the check. It runs only for CLIs that declare their composer glyph, which today means Claude Code and Codex. - Send-and-wait registers the waiter before writing, so the wait cannot report the previous turn.
API=http://localhost:3000 # ID is the session id from GET /api/sessions
curl -s -X POST "$API/api/sessions/$ID/input" \
-H 'Content-Type: application/json' \
-d '{"input":"summarize src/session.ts\r","useMux":true,"wait":"stop","waitTimeout":120000}'
The full rules for scripting sessions, including the trust dialog and readiness, are in Driving Codeman From An Agent and the HTTP API reference.