Skip to content
HN On Hacker News ↗

tmux + Claude Code: Sessions, send-keys, and the Ceiling

▲ 174 points • 128 comments • by ankitg12 • 2w ago • HN discussion ↗

Pangram verdict · v3.3

We believe that this entire text is AI.

100 %

AI likelihood · overall

AI
0% human-written 100% AI-generated
SEGMENTS · HUMAN 0 of 1
SEGMENTS · AI 1 of 1
WORD COUNT 1,668
PEAK AI % 100% · §1
Analyzed
Oct 19
backend: pangram/v3.3
Segments scanned
1 windows
avg 1668 words each
Distribution
0 / 100%
human / AI fraction
Verdict
AI
Pangram v3.3

Article text · 1,668 words · 1 segments analyzed

Human AI-generated
§1 AI · 100%

tmux runs your AI coding agents in named, detachable sessions that survive a closed terminal or a dropped SSH connection. You get one window holding many agents, send-keys to type into any of them from a script, and capture-pane to read one back. That combination is why people use it. Before you build any of it by hand, check the built-in path: recent versions of Claude Code can create the worktree and the tmux session for you with claude --worktree --tmux. This guide covers that first, then everything it doesn’t reach, then the point where tmux itself stops. Why tmux and agents go together A coding agent is a long-running process that wants to keep talking to a terminal. Three properties of tmux match that shape: It survives disconnection. The tmux server owns the pty, not your terminal window. Close the window, lose your SSH link, put the laptop to sleep: the agent keeps working and is still there when you reattach. It holds many sessions in one window. Four agents become four named sessions you switch between with a keystroke, instead of four windows you hunt for in the app switcher. It’s scriptable from outside. Any shell, anywhere, can type into a running agent or read its screen. No IPC to build, no API to wire up. This is the property people build on, and it’s why tmux keeps showing up in agent-orchestration scripts. The built-in flag, before you build anything Claude Code ships with this wired up for the single-agent-on-a-branch case. Two flags, from claude --help on a recent version: -w, --worktree [name] Create a new git worktree for this session (optionally specify a name) --tmux Create a tmux session for the worktree (requires --worktree). Uses iTerm2 native panes when available; use --tmux=classic for traditional tmux. So the whole “isolate this task, give it its own session” move is one command: claude --worktree --tmux # or name the worktree claude --worktree auth-refactor --tmux Two details matter before you rely on it. --tmux errors without --worktree, so it’s a worktree feature that happens to make a session, not a general session flag. And on iTerm2 it uses native panes instead of tmux, which is nicer to look at and means the tmux commands in the rest of this page have nothing to attach to. Pass --tmux=classic when you want real tmux underneath, which you do if you plan to script against the session. For a single agent on a single branch, that’s the answer. Use it and skip ahead. What it doesn’t do: run several agents in one session, lay out panes so you can watch them, let you type into a running agent from a script, tell you which agent is waiting on you, or set any of this up on a remote box over SSH. Those are the rest of this guide, and each one is where people end up writing tmux by hand. More on the worktree half of that flag in git worktrees for parallel development. Session per project, named by project The organizing rule that keeps this usable: one tmux session per repo, named after the repo. Not per task, not per agent. Tasks come and go; the project is the stable thing. # start a detached session in a project directory tmux new-session -d -s api -c ~/code/api # attach to it (creates it if it doesn't exist) tmux new-session -A -s api -c ~/code/api -A is attach-or-create. It makes a single command safe to run repeatedly, which is what you want in a shell function: # ~/.zshrc agent() { local dir="${1:-$PWD}" local name="$(basename "$dir" | tr . -)" tmux new-session -A -s "$name" -c "$dir" } Session names can’t contain . or : (tmux uses them as target separators), hence the tr. Then tmux ls becomes a real inventory: $ tmux ls api: 3 windows (created Fri Jul 31 09:12:04 2026) web: 2 windows (created Fri Jul 31 09:40:11 2026) infra: 1 windows (created Fri Jul 31 11:02:55 2026) Add a bit more with a format string: tmux list-sessions -F '#{session_name} windows=#{session_windows} attached=#{session_attached}' A layout for watching several agents Inside a session, give each agent its own window, named for the task: tmux new-window -t api -n auth -c ~/code/api tmux new-window -t api -n tests -c ~/code/api Windows are better than panes for agents you’re not actively watching, because a window costs no screen space. Panes are for the two or three you want side by side right now: # split the current window and tile whatever is in it tmux split-window -h -c '#{pane_current_path}' tmux split-window -v -c '#{pane_current_path}' tmux select-layout tiled #{pane_current_path} is easy to leave off. Without it a new pane opens in the directory the session started in, not the directory you’re actually in, and the agent you launch there is pointed at the wrong repo. Two layouts by name: tiled for an even grid of agents, and main-vertical for one big pane (the agent you’re driving) with the rest stacked beside it. tmux select-layout main-vertical tmux set-window-option main-pane-width 60% Percentage values for main-pane-width need tmux 3.2 or newer. On older builds use an absolute column count (main-pane-width 120). Check with tmux -V. A .tmux.conf worth having These are the lines that matter for agent work specifically. They’re not a full config, they’re the delta from defaults that a fleet of agents makes you want. # agents produce a lot of output, and you will want to scroll back through it set -g history-limit 100000 # 24-bit colour, so agent TUIs render the way they were designed to set -g default-terminal "tmux-256color" set -ga terminal-overrides ",*256col*:Tc" # scroll and click between panes with the mouse set -g mouse on # don't let a running process rename the window out from under you set -g allow-rename off set-window-option -g automatic-rename off # 1-indexed windows match the number keys on your keyboard set -g base-index 1 set-window-option -g pane-base-index 1 set -g renumber-windows on # flag windows whose agent produced output while you were elsewhere set-window-option -g monitor-activity on set -g visual-activity off # status line refreshes often enough to be a monitor set -g status-interval 5 # no delay when an agent's TUI reads an escape sequence set -sg escape-time 10 # jump panes without the prefix key bind -n M-h select-pane -L bind -n M-j select-pane -D bind -n M-k select-pane -U bind -n M-l select-pane -R # reload without restarting the server bind r source-file ~/.tmux.conf \; display-message "config reloaded" Agent TUIs like to set the terminal title, so without allow-rename off your carefully named auth window becomes whatever the agent felt like calling itself. escape-time 10 fixes the “my arrow keys are laggy inside the agent” complaint. The default delay exists to disambiguate a real Escape key from an escape sequence, and it makes interactive TUIs feel mushy. Driving an agent with send-keys send-keys types into a pane as if you were at the keyboard. This is how you script an agent from cron, from a git hook, or from another agent. # target syntax: session:window.pane tmux send-keys -t api:auth "run the test suite and fix what fails" Enter Three things go wrong here, in order of how often: Bare arguments get interpreted as key names. Enter, C-c, Escape, and Space are keys, not text. If your prompt contains one of those words as a standalone argument it gets sent as a keypress. Use -l to send a literal string: tmux send-keys -t api:auth -l "press Enter to continue was the old flow" tmux send-keys -t api:auth Enter -l disables key-name lookup entirely, so the newline has to be a second call. Semicolons end the tmux command. A trailing ; in your text is read as a command separator. Escape it as \;. Agent TUIs need a beat between the text and the newline. Many agent input boxes debounce or re-render on paste, and an Enter arriving in the same frame as the text gets swallowed. The reliable pattern: tmux send-keys -t api:auth -l "$PROMPT" sleep 0.3 tmux send-keys -t api:auth Enter Interrupting a runaway agent is the same mechanism: tmux send-keys -t api:auth C-c Reading an agent back with capture-pane capture-pane dumps a pane’s contents to stdout. It’s the read half of the pair. # the visible screen tmux capture-pane -p -t api:auth # the last 200 lines, with wrapped lines joined back together tmux capture-pane -p -J -S -200 -t api:auth # the entire scrollback tmux capture-pane -p -J -S - -t api:auth # keep the colour escape sequences tmux capture-pane -p -e -t api:auth -J matters more than it looks. Without it, a line that wrapped at your terminal width comes back as two lines, and every grep you write against the output is wrong at the seams. For a continuous log rather than a snapshot, pipe-pane streams everything a pane produces to a command: tmux pipe-pane -o -t api:auth 'cat >> ~/agent-logs/api-auth.log' # stop it tmux pipe-pane -t api:auth -o toggles, so running it twice turns it off. The output is raw, escape sequences and all, so pipe it through something like sed -E 's/\x1b\[[0-9;]*[a-zA-Z]//g' if you want it readable later. The status-line monitor Every person who runs several agents in tmux eventually builds this: a status line that tells you which agents are working and which are waiting. The mechanism is #() in the status line, which runs a shell command and interpolates its output, refreshed every status-interval seconds. set -g status-interval 5 set -g status-right '#(~/bin/tmux-agent-status) | %H:%M' set -g status-right-length 100 And the script, which capture-scrapes each pane for a sign of life: #!/usr/bin/env bash # ~/bin/tmux-agent-status out="" while read -r target; do screen="$(tmux capture-pane -p -J -S -8 -t "$target" 2>/dev/null)" name="${target%%:*}" case "$screen" in *"Do you want"*|*"❯ 1."*) out+="⏸ $name " ;; # parked on a prompt *"esc to interrupt"*) out+="● $name " ;; # working *) out+="○ $name " ;; # idle esac done < <(tmux list-panes -a -F '#{session_name}:#{window_index}.#{pane_index}') printf '%s' "$out"