商业产业 Hacker News AI 2026-10-19T05:55 正文 ok 2766 words

Tmux 与 Claude Code:会话管理、send-keys 自动化与能力天花板

英文标题:🔶 Tmux and Claude Code: Sessions, Send-Keys, and the Ceiling

本文探讨了如何利用 tmux 管理与调度 Claude Code 等 AI 编程 Agent。借助 tmux 的后台持久化、单窗口多会话聚合以及通过 send-keys 与 capture-pane 实现的无 IPC 脚本化交互,开发者能以极低成本构建高并发、可复原的 AI 编程工作流。文章深入解析了 Claude Code 内置的 --worktree --tmux 支持、以项目为维度的 Session 组织规范、关键配置优化及 SSH 远程开发实践,同时也指出了通过终端抓取推断 Agent 状态的能力边界。
打开原文相关 3 · 机会 0
media3.6 KB media245.7 KB media37.4 KB media35.2 KB media163.2 KB

中文

tmux 可以在具名的、可脱离(detachable)的会话中运行你的 AI 编程 Agent,即使关闭终端或 SSH 连接断开,会话依然存活。你可以在一个窗口中容纳多个 Agent,使用 send-keys 从脚本向其中任意一个输入指令,并用 capture-pane 读取其终端回显。正是这种组合,让 tmux 成为了大家的首选工具。

在你动手从头搭建前,请先了解其内置支持:近期版本的 Claude Code 已经支持通过 claude --worktree --tmux 自动为你创建 worktree 和 tmux 会话。本指南首先介绍该功能,随后探讨它未覆盖的进阶场景,最后指出 tmux 本身的能力边界。

为什么 tmux 与 Agent 是绝配

编程 Agent 是一个需要持续与终端交互的长生命周期进程。tmux 的三个特性恰好完美契合这一形态:

断连依然存活。 pty(伪终端)由 tmux 服务端掌控,而非你的终端窗口。关闭窗口、断开 SSH 链接、合上笔记本电脑休眠:Agent 都会在后台继续运行,重新 attach 后一切完好如初。

单一窗口容纳多个会话。 四个 Agent 会变成四个具名会话,只需一个快捷键即可在它们之间切换,而不需要在系统的应用切换器中翻找四个独立的终端窗口。

支持外部脚本化调用。 任何位置的任何 shell 都可以向正在运行的 Agent 输入内容或读取其屏幕画面。无需构建 IPC,无需对接 API。这是开发者构建自动化所依赖的核心特性,也是 tmux 频繁出现在各类 Agent 编排脚本中的原因。

在手写脚本之前:Claude Code 的内置参数

Claude Code 为“单个分支上运行单 Agent”的场景内置了开箱即用的支持。在近期版本中运行 claude --help 可以看到这两个参数:

-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.

因此,“隔离该任务并赋予其专属会话”的操作只需一条命令:

bash claude --worktree --tmux

或者为 worktree 命名

claude --worktree auth-refactor --tmux

在依赖它之前需要注意两个细节。如果不加 --worktree,--tmux 会直接报错;因此它本质上是一个附带创建会话功能的 worktree 特性,而非通用的会话参数。此外,在 iTerm2 上它会默认使用原生分屏(native panes)而非标准 tmux。虽然视觉体验更好,但这也意味着本文后续介绍的 tmux 命令将无法 attach 到该会话。如果你打算对会话进行脚本化操作,请传入 --tmux=classic 以使用底层原生 tmux。

对于在单一分支上运行单 Agent 的情况,内置参数就是最佳答案。直接使用它即可跳过后续基础部分。

但它无法做到的是:在单个会话中运行多个 Agent、平铺窗格以便实时监控、通过外部脚本向运行中的 Agent 键入指令、提示哪个 Agent 正在等待人工介入,或通过 SSH 在远程机器上完成上述整套配置。这些正是本指南后续要探讨的内容,也是开发者最终需要手写 tmux 配置的原因。

关于该参数中 worktree 部分的更多信息,请参阅用于并行开发的 git worktrees。

按项目划分 Session,以项目命名

保持整套方案整洁可用的核心组织原则是:每个仓库分配一个 tmux session,并以仓库名命名。不要按任务划分,也不要按 Agent 划分。任务会来来去去,而项目才是稳定的主体。

bash

在项目目录中启动一个后台分离会话

tmux new-session -d -s api -c ~/code/api

attach 到该会话(如果不存在则创建)

tmux new-session -A -s api -c ~/code/api

-A 代表 attach-or-create(存在则连接,不存在则创建)。它保证了单条命令可安全重复运行,非常适合封装进 shell 函数:

bash

~/.zshrc

agent() { local dir="${1:-$PWD}" local name="$(basename "$dir" | tr . -)" tmux new-session -A -s "$name" -c "$dir" }

由于 tmux 使用 . 和 : 作为目标标识符的分隔符,因此 Session 名称中不能包含这些字符,需要通过 tr 进行转换。

这样一来,tmux ls 就能呈现清晰的资产清单:

$ 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)

还可以加上格式化字符串展示更丰富的信息:

bash tmux list-sessions -F '#{session_name} windows=#{session_windows} attached=#{session_attached}'

监控多个 Agent 的窗口布局

在会话内部,为每个 Agent 分配独立的 window,并以任务命名:

bash tmux new-window -t api -n auth -c ~/code/api tmux new-window -t api -n tests -c ~/code/api

对于不需要实时紧盯的 Agent,使用 window 优于 pane(分屏窗格),因为后台 window 不占用任何屏幕空间。Pane 则用于放置当下你想要并排观察的两到三个 Agent:

bash

拆分当前窗口并将其中内容平铺

tmux split-window -h -c '#{pane_current_path}' tmux split-window -v -c '#{pane_current_path}' tmux select-layout tiled

参数 #{pane_current_path} 很容易被忽略。如果遗漏该参数,新分屏将在会话启动的初始目录中打开,而非你当前所在的目录,导致新启动的 Agent 靶向错误的仓库。

推荐使用两个布局预设:tiled 呈现均匀网格分布;main-vertical 则提供一个主屏大窗格(用于当前操作的 Agent),其余窗格垂直堆叠在侧边。

bash tmux select-layout main-vertical tmux set-window-option main-pane-width 60%

main-pane-width 的百分比语法需要 tmux 3.2 或更高版本。在旧版本中请使用固定列数绝对值(如 main-pane-width 120)。可通过 tmux -V 查看版本。

值得配置的 .tmux.conf

以下是专为 Agent 任务优化的关键配置项。这不是一份完整的配置文件,而是针对多 Agent 并发场景对默认值进行的针对性改造。

tmux

Agent 会产生大量输出,你需要回滚翻查历史记录

set -g history-limit 100000

启用 24 位真彩色,保证 Agent 的终端 UI 正常渲染

set -g default-terminal "tmux-256color" set -ga terminal-overrides ",256col:Tc"

支持鼠标滚轮滚动及点击切换窗格

set -g mouse on

禁止运行中的进程随意篡改窗口名称

set -g allow-rename off set-window-option -g automatic-rename off

窗口从 1 开始编号,与键盘上的数字键对应

set -g base-index 1 set-window-option -g pane-base-index 1 set -g renumber-windows on

当你离开某个窗口时,标记其中产生新输出的 Agent 窗口

set-window-option -g monitor-activity on set -g visual-activity off

状态栏刷新频率足够高,以便充当监控面板

set -g status-interval 5

消除 Agent TUI 读取转义序列(Escape Sequence)时的延迟

set -sg escape-time 10

无需按下前缀键(Prefix)即可在窗格间快速跳转

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

重载配置无需重启 tmux 服务端

bind r source-file ~/.tmux.conf ; display-message "config reloaded"

Agent 的 TUI 程序通常喜欢修改终端标题,如果没有配置 allow-rename off,你精心命名的 auth 窗口就会被覆盖为 Agent 自行指定的名称。

escape-time 10 解决了“在 Agent 内部使用方向键卡顿迟滞”的问题。默认延迟是为了区分用户按下的是单独的 Escape 键还是转义序列,但这会导致交互式 TUI 产生黏滞感。

使用 send-keys 驱动 Agent

send-keys 能够像你在物理键盘前打字一样向窗格输入内容。你可以借此通过 cron 定时任务、git hook 或另一个 Agent 来编写自动化脚本控制当前的 Agent。

bash

目标语法:session:window.pane

tmux send-keys -t api:auth "run the test suite and fix what fails" Enter

在此处最容易出现三个问题,按常见程度排序如下:

独立的英文单词被识别为按键名称。 Enter、C-c、Escape 和 Space 是功能键而不是普通文本。如果你的 Prompt 中包含这些单词作为独立参数,它会被当作按键发送。使用 -l 可以发送字面纯文本字符串:

bash tmux send-keys -t api:auth -l "press Enter to continue was the old flow" tmux send-keys -t api:auth Enter

由于 -l 会彻底禁用按键名称解析,因此换行回车必须通过第二次调用单独发送。

分号会截断 tmux 命令。 文本末尾的分号 ; 会被解析为命令分隔符。需要转义为 \;。

Agent 的 TUI 需要在文本输入与回车之间留出间隔。 许多 Agent 的输入框在粘贴内容时会有防抖(debounce)或重绘逻辑;如果回车与文本在同一帧到达,回车可能会被吞掉。稳妥的操作范式如下:

bash tmux send-keys -t api:auth -l "$PROMPT" sleep 0.3 tmux send-keys -t api:auth Enter

中断失控运行的 Agent 也是使用同样的机制:

bash tmux send-keys -t api:auth C-c

使用 capture-pane 读取 Agent 输出

capture-pane 用于将窗格内容倾倒入标准输出(stdout),它是读取操作的核心对偶工具。

bash

抓取当前可见屏幕

tmux capture-pane -p -t api:auth

抓取最后 200 行,并将被折行的文本重新合并

tmux capture-pane -p -J -S -200 -t api:auth

抓取全部滚动历史缓冲区(scrollback)

tmux capture-pane -p -J -S - -t api:auth

保留颜色等转义序列

tmux capture-pane -p -e -t api:auth

-J 参数的作用远比表面看起来重要。没有它,在终端宽度处折行的单行文本会被拆成两行返回,所有针对输出编写的正则 grep 操作都会在接缝处断裂报错。

如果需要持续记录日志而非单次快照,pipe-pane 可以将窗格产生的所有输出实时流式传输至指定命令:

bash tmux pipe-pane -o -t api:auth 'cat >> ~/agent-logs/api-auth.log'

停止记录

tmux pipe-pane -t api:auth

-o 是开关切换(toggle)参数,重复执行即可关闭。输出内容是原始数据(包含转义序列),如果希望后续便于阅读,可将其通过诸如 sed -E 's/\x1b\[[0-9;]*[a-zA-Z]//g' 等命令过滤处理。

状态栏监控器

每一个在 tmux 中并发运行多个 Agent 的开发者,最终都会实现类似的功能:一个能显示哪些 Agent 正在工作、哪些正在等待的状态栏。

其实现机制是状态栏配置中的 #() 语法,它会执行 shell 命令并嵌入其输出,每隔 status-interval 秒刷新一次。

tmux set -g status-interval 5 set -g status-right '#(~/bin/tmux-agent-status) | %H:%M' set -g status-right-length 100

配套脚本通过抓取并解析各个窗格来判断活跃状态:

bash #!/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 " ;; # 停留在等待用户确认的提示符 "esc to interrupt") out+="● $name " ;; # 正在工作中 *) out+="○ $name " ;; # 空闲 esac done < <(tmux list-panes -a -F '#{session_name}:#{window_index}.#{pane_index}') printf '%s' "$out"

这套方案行之有效,且值得搭建。但务必清楚它的底层逻辑:它本质上是在每 5 秒一次的轮询中,对每个已打开窗格渲染出的终端画面进行字符串匹配。而匹配的关键词完全取决于你当前使用的 Agent 版本碰巧输出了什么提示词。

更轻量级的方案是使用 tmux 自带的活动跟踪,而非屏幕抓取。monitor-silence 可以在窗格持续 N 秒没有输出时触发警告,这是原生机制中最接近“该 Agent 已停止”的信号:

tmux set-window-option -g monitor-silence 30 set -g visual-silence on

你还可以为其挂载钩子动作:

tmux set-hook -g alert-silence 'run-shell "osascript -e "display notification \"agent idle\"""'

需要注意的是,“静默”只是完成状态的一种粗略推导,而非确切报告。Agent 在针对大型文件进行深度思考时,同样也会处于静默状态。

Detach、Reattach 与断连存活机制

bash tmux detach-client # 或按下 Prefix 前缀键,随后按 d tmux attach -t api tmux switch-client -t web # 在另一个会话内部进行切换 tmux kill-session -t api

各类事件的存活情况:

事件 Agent 是否继续运行?
关闭终端窗口 是
Detach 客户端 是
SSH 连接断开 是
tmux kill-session 否
tmux kill-server 否
系统重启 否
macOS 重启后的“重新打开所有窗口” 否

tmux 服务端归根结底只是当前用户名下的一个常规进程。tmux 本身没有任何机制能在机器重启后幸存,一旦服务端停止,会话中的历史回滚数据就会彻底丢失。如果交互转录记录至关重要,请通过 pipe-pane 转储到文件,或使用具备自身持久化历史功能的工具。

另一个容易踩坑的细节:处于 Detach 状态的会话窗格大小会被固定为上一个连接客户端的分辨率;如果此前从未连接过客户端,则默认为 80x24。如果离线期间 Agent TUI 在 80 列的窗格中进行渲染,当你以 200 列重新连接时,界面布局在重绘之前可能会出现错乱。配置 set-window-option -g aggressive-resize on 有助于解决多个客户端以不同尺寸连接时的适配问题。

通过 SSH 运行 tmux

在不可靠的网络链路下通过远程主机运行 Agent,是 tmux 真正难以被替代的核心场景:

bash

单条命令完成 attach 或创建,并分配 tty 终端

ssh devbox -t tmux new-session -A -s agents

任务执行中途网络断开,只需重新执行该命令,便能重回同一会话,而 Agent 依然在持续工作。如果网络条件极差、SSH 本身频繁断开,可将其与 mosh 配合使用。

嵌套 tmux 问题:如果在本地和远程服务器上均运行 tmux,两者会争抢同一个前缀键(Prefix)。解决方案有两种:在远程机器上改用不同的前缀键(set -g prefix C-a),或者连按两次前缀键(通过 bind C-b send-prefix 支持)。

关于在非本地物理机上运行 Agent 的实践,请参阅远程开发。

tmux 止步于何处

tmux 是一款出色的软件。它几乎完美解决了终端复用与持久化问题,且自 2007 年起就经受住了考验。但它不具备任何针对 Agent 特化的高级特性——毕竟在它的模型被设计出来时,AI Agent 根本还不存在。

无法直观感知哪个 Agent 处于停滞状态。 正在等待审批的 Agent...

English / 原文

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"

It works, and it’s worth building. Be clear-eyed about what it’s doing: string matching against a TUI’s rendered frame, on a five-second poll, for every pane you have open. The match strings are whatever your agent version happens to print this week.

The lighter-weight version uses tmux’s own activity tracking instead of scraping. monitor-silence fires an alert when a pane produces no output for N seconds, which is the closest native signal to “this agent stopped”:

set-window-option -g monitor-silence 30
set -g visual-silence on

You can hang an action off it:

set-hook -g alert-silence 'run-shell "osascript -e \"display notification \\\"agent idle\\\"\""'

Silence is a proxy for done, not a report of it. An agent thinking hard about a large file is silent too.

Detach, reattach, and what survives what

tmux detach-client            # or the prefix, then d
tmux attach -t api
tmux switch-client -t web     # from inside another session
tmux kill-session -t api

What survives:

Event Agents keep running?
Close the terminal window yes
Detach the client yes
SSH connection drops yes
tmux kill-session no
tmux kill-server no
Reboot no
macOS “Reopen windows” after restart no

The tmux server is just a process owned by your user. Nothing about tmux makes it survive a reboot, and a session’s scrollback is lost when the server stops. If the transcript matters, pipe-pane it to a file or use a tool that persists history on its own.

One detail people trip on: a detached session’s panes are sized to the last client that was attached, or to 80x24 if there never was one. An agent TUI rendering into an 80-column pane while you’re away will look wrong when you reattach at 200 columns, until it redraws. set-window-option -g aggressive-resize on helps when several clients are attached at different sizes.

tmux over SSH

This is where tmux is genuinely hard to replace. Running agents on a remote box, over a link you don’t trust:

# attach or create, in one command, with a tty allocated
ssh devbox -t tmux new-session -A -s agents

Your connection drops mid-task, you run the same command again, and you’re back in the same session with the agent still going. Pair it with mosh if your link is bad enough that SSH itself keeps dying.

The nested-tmux problem: if you run tmux locally and also on the remote box, both grab the same prefix. The two fixes are a different prefix on the remote (set -g prefix C-a), or pressing the prefix twice, which bind C-b send-prefix gives you.

For running agents on a machine that isn’t the one in front of you, see remote development.

Where tmux stops

tmux is good software. It solves multiplexing and persistence about as well as those can be solved, and it has been doing it since 2007. What it doesn’t do is anything specific to agents, because agents didn’t exist when its model was designed.

No visibility into which agent is parked. An agent waiting on an approval prompt looks exactly like an agent thinking, which looks exactly like an agent that finished twenty minutes ago. All three are “a pane with some text in it”. The status-line hack above exists to paper over this, and it papers by scraping.

No notification when one finishes. monitor-silence is the nearest thing, and it fires on any quiet pane for any reason. There is no event for “the agent is done” because tmux has no idea what an agent is.

Screen scraping is brittle by construction. capture-pane gives you the rendered frame: spinners, box drawing, truncated columns, ANSI. Not what the agent said, not which tool it called, not whether that call succeeded. Every parser you write against it breaks when the agent ships a UI change.

No shared state across sessions. Two agents in two tmux sessions have no way to see each other. If you want one to read another’s transcript or hand off work, you build that yourself, out of files and locks and polling.

Scrollback for several streaming agents is expensive. history-limit 100000 across a dozen panes of agent output is real memory, held per pane for as long as the server lives, and gone the moment it does. See terminal emulators for AI agents for why the rendering side of this gets costly too.

Nothing survives a reboot. Every session, every transcript, gone.

None of these are flaws. They are the edge of a tool built to multiplex shells for one person at a keyboard.

Where crystl picks up

crystl is a macOS terminal built for the workload tmux was not shaped for: many agents at once, most of them off-screen, each one occasionally needing you.

The differences map almost one-to-one onto the list above.

  • Approvals surface instead of getting buried. When any agent pauses for permission, crystl floats it as a card, colour-coded by project, and you answer without switching sessions. No scraping, no polling. See floating cards.
  • Agent state is a first-class signal. The agent activity panel shows every session’s state at once: working, waiting on you, idle, done. That’s the thing the status-line script was approximating.
  • Output is structured, not scraped. crystl captures each turn, tool call, and result as typed records, so history is searchable across every session and agents can read each other’s transcripts without parsing ANSI.
  • Sessions are organized by project, and they persist. Projects are gems, sessions inside them are shards, and history survives closing a window or restarting the Mac. More in multitasking.
  • Parallel work on one repo is isolated. Isolated shards are backed by real git worktrees, so two agents on the same repo don’t collide. See parallel sessions.

The recommendation: keep tmux. It’s still the right answer for remote boxes, for long-running processes on servers, and for anything where the SSH link is the fragile part. Use it for that. When the thing you’re managing is six agents that each need a decision from you at an unpredictable moment, the problem stopped being multiplexing.

More on the organizing side of this in managing multiple AI coding sessions, and on keeping agents working rather than idle in running AI coding agents for longer.