Hacker News 中文摘要

RSS订阅

Tmux and Claude Code: Sessions, Send-Keys, and the Ceiling

终端复用器(tmux)让你的 AI 编码代理运行在具名且可分离的会话里,即使关闭终端或 SSH 断开也能存活;一个窗口里可以容纳多个代理,还能用 send-keys 从脚本向任意代理输入、用 capture-pane 读回内容。在手动搭建这些之前,先看内置方式:近期版本的克劳德代码(Claude Code)可用 claude --worktree --tmux 直接创建工作树和 tmux 会话;指南会先讲这条路,再讲它覆盖不到的部分,以及 tmux 本身止步的地方。

正文

tmux 可以把 AI 编程智能体跑在命名且可分离的会话里,即使关掉终端或 SSH 断连,会话依然存在。你只用一个窗口就能容纳多个智能体,用 send-keys 从脚本往任意一个里面“打字”,再用 capture-pane 把输出读回来。正是这几点的组合,让大家选择 tmux。

在手动搭建之前,先看内置方案:较新版本的 Claude Code 能用 claude --worktree --tmux 直接帮你建好工作树和 tmux 会话。这里先讲这个内置方案,再讲它覆盖不到的部分,最后是 tmux 本身的能力边界。

为什么 tmux 和智能体天生契合

编程智能体是需要长时间运行、并持续与终端交互的进程。tmux 有三个特性正好匹配这种需求。

第一,断连后依然存活。tmux 服务端拥有 pty,而不是你的终端窗口。关掉窗口、SSH 断开、合上笔记本,智能体都会继续运行,等你重新连上还在那里。

第二,一个窗口里管理多个会话。四个智能体就是四个命名会话,按一次键就能切换,不用在应用切换器里找四个窗口。

第三,可以从外部脚本化操作。任何 shell 都能向正在运行的智能体发送输入,或者读取它的屏幕内容,不需要自己搭 IPC,也不需要接 API。大家正是基于这一点来构建,这也是 tmux 频繁出现在智能体编排脚本里的原因。

动手之前,先试内置标志

Claude Code 本身就内置了单分支单智能体的支持。在较新版本上运行 claude --help,能看到两个标志:-w, --worktree [name] 用于为本次会话创建新的 git 工作树(可选指定名称);--tmux 则为该工作树创建 tmux 会话(依赖 --worktree,在 iTerm2 上会优先使用原生面板,传统 tmux 需显式 --tmux=classic)。

因此,“隔离任务并给它独立会话”只需一条命令:claude --worktree --tmux,或者指定工作树名称 claude --worktree auth-refactor --tmux。

有两点要先弄清楚。--tmux 在没有 --worktree 时会报错,所以它本质上是一个“顺带创建会话”的工作树功能,而不是通用的会话标志。另外,在 iTerm2 上它会用原生面板代替 tmux,虽然更好看,但这意味着后面讲到的 tmux 命令将没有可依附的对象;如果你打算用脚本操作会话,就要传 --tmux=classic 来启用真正的 tmux。

如果只是单分支上的单个智能体,这就是答案,直接用它,后面的可以跳过。

它做不到的事情包括:在一个会话里跑多个智能体、排布面板方便观察、从脚本向运行中的智能体输入、告诉你哪个智能体在等你,或者通过 SSH 在远程机器上完成这些设置。这些正是接下来要讲的,也是人们最终手写 tmux 的场景。

关于该标志中工作树部分的更多内容,见 git worktrees for parallel development。

每个项目一个会话,按项目命名

让这一切保持可用的组织原则是:一个代码仓库对应一个 tmux 会话,且用仓库名命名。不是按任务,也不是按智能体——任务来了又走,项目才是稳定的东西。

启动分离会话用 tmux new-session -d -s api -c ~/code/api;带 -A(附着或创建)的则是 tmux new-session -A -s api -c ~/code/api。-A 让这条命令可以安全地反复执行,适合写进 shell 函数,例如在 ~/.zshrc 里定义 agent():local dir="${1:-$PWD}"; local name="$(basename "$dir" | tr . -)"; tmux new-session -A -s "$name" -c "$dir"。

会话名不能包含 . 和 :,因为 tmux 把它们用作目标分隔符,所以上面用了 tr 做替换。

之后,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)。想多看一些信息,可以用格式字符串:tmux list-sessions -F '#{session_name} windows=#{session_windows} attached=#{session_attached}'。

同时观察多个智能体的布局

在会话内部,给每个智能体分配一个以任务命名的窗口:

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

对于不主动盯着的智能体,窗口比面板更合适,因为它不占屏幕空间。面板适合用来并排看两三个智能体:用 tmux split-window -h -c '#{pane_current_path}' 和 tmux split-window -v -c '#{pane_current_path}' 切分,再执行 tmux select-layout tiled 平铺。

这里的 #{pane_current_path} 很容易漏掉。如果不加,新面板会在会话启动时的目录打开,而不是你当前所在的目录,这样启动的智能体就会指向错误的代码库。

两种常用布局:tiled 是均匀网格,适合平铺多个智能体;main-vertical 是一个大面板(你正在操作的智能体)加旁边堆叠的其他面板。命令是 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

下面是一些配智能体工作时特别有用的配置(不是完整配置,只是相对默认值需要改的部分):set -g history-limit 100000(智能体输出多,方便回滚);set -g default-terminal "tmux-256color" 与 set -ga terminal-overrides ",*256col*:Tc"(开启 24 位色,让 TUI 正常渲染);set -g mouse on(鼠标滚动和点击切换面板);set -g allow-rename off 与 set-window-option -g automatic-rename off(禁止运行中的进程乱改窗口名);set -g base-index 1、set-window-option -g pane-base-index 1、set -g renumber-windows on(窗口和面板从 1 开始编号,对应键盘数字键);set-window-option -g monitor-activity on 与 set -g visual-activity off(有输出时标记窗口);set -g status-interval 5(状态栏刷新更及时);set -sg escape-time 10(修复智能体 TUI 里方向键卡顿);无前缀切换面板的 bind -n M-h select-pane -L 等(M-j、M-k、M-l 同理对应下、上、右);以及 bind r source-file ~/.tmux.conf \; display-message "config reloaded" 不重启服务端重载配置。

智能体 TUI 喜欢自己设置终端标题,所以如果不关掉 allow-rename,你精心命名的 auth 窗口会被智能体改成它自己想起的名字。escape-time 10 则解决了“智能体里方向键发黏”的抱怨:默认延迟是为了区分真正的 Esc 键和转义序列,但会让交互式 TUI 显得迟钝。

用 send-keys 驱动智能体

send-keys 能像人在键盘上输入一样向面板发送内容,因此你可以用 cron、git hook 或另一个智能体来脚本化地驱动某个智能体。目标地址写法是 session:window.pane,例如 tmux send-keys -t api:auth "run the test suite and fix what fails" Enter。

这里常踩三个坑。第一,裸参数会被当成键名。Enter、C-c、Escape、Space 都是按键而非文本;如果你的提示词里独立包含这些词,它们会被当作按键发出去。用 -l 可以发送字面字符串,但 -l 会完全禁用键名解析,所以换行必须再单独发一次:先 tmux send-keys -t api:auth -l "press Enter to continue was the old flow",再 tmux send-keys -t api:auth Enter。

第二,分号会结束 tmux 命令。文本结尾的 ; 会被当成命令分隔符,要转义成 \;。

第三,智能体 TUI 在文本和换行之间需要一点间隔。很多输入框在粘贴时会防抖或重绘,如果 Enter 和文本在同一帧到达,可能会被吞掉。可靠的写法是:先 tmux send-keys -t api:auth -l "$PROMPT",然后 sleep 0.3,再发 tmux send-keys -t api:auth Enter。

想中断失控的智能体,也是同样的机制:tmux send-keys -t api:auth C-c。

用 capture-pane 读取智能体输出

capture-pane 把面板内容输出到 stdout,是这一对操作里的“读”。常用形式有:tmux capture-pane -p -t api:auth 取当前可见屏幕;tmux capture-pane -p -J -S -200 -t api:auth 取最后 200 行并把换行接回原样;tmux capture-pane -p -J -S - -t api:auth 取全部回滚缓冲;tmux capture-pane -p -e -t api:auth 保留颜色转义序列。

其中的 -J 比看起来更重要。不加的话,一行在终端宽度处折回会变成两行,你写的任何 grep 都会在接缝处出错。

如果想持续记录而不是抓快照,可以用 pipe-pane 把面板产生的一切流传给某个命令:tmux pipe-pane -o -t api:auth 'cat >> ~/agent-logs/api-auth.log',停止时执行 tmux pipe-pane -t api:auth。-o 是切换开关,再跑一次就关闭。输出是原始的、带转义序列的,若想之后可读,可以过一遍 sed -E 's/\x1b\[[0-9;]*[a-zA-Z]//g'。

状态栏监控

只要在 tmux 里同时跑几个智能体,大家最后都会做一个状态栏,用来显示哪些智能体在忙、哪些在等待。

原理是状态栏里的 #():它会运行 shell 命令并把输出插入状态栏,每 status-interval 秒刷新一次。配置如 set -g status-interval 5、set -g status-right '#(~/bin/tmux-agent-status) | %H:%M'、set -g status-right-length 100。

配套的 ~/bin/tmux-agent-status 脚本会对每个面板抓取屏幕内容来判断状态:用 capture-pane -p -J -S -8 取最近几行,如果匹配到 "Do you want" 或 "❯ 1." 就显示 ⏸(停在提示上),匹配到 "esc to interrupt" 显示 ●(工作中),否则显示 ○(空闲)。脚本遍历 tmux list-panes -a -F '#{session_name}:#{window_index}.#{pane_index}' 的所有面板并输出结果。

这个办法能用,也值得做,但要清楚它的本质:在 5 秒一次的轮询里,对所有面板做字符串匹配,比对的还是 TUI 渲染出来的画面;匹配用的字符串完全取决于你的智能体这个版本刚好打印了什么。

更轻量的做法是用 tmux 自带的活跃度追踪。monitor-silence 会在某个面板 N 秒内没有输出时触发提醒,这是最接近“这个智能体停了”的原生信号:set-window-option -g monitor-silence 30 加 set -g visual-silence on。你还可以挂个动作,比如 set-hook -g alert-silence 'run-shell "osascript -e \"display notification \\\"agent idle\\\"\""'。不过静默只是“可能做完了”的代理指标,不是完成报告——智能体在啃大文件时也会沉默。

分离、重连,以及什么情况下能存活

分离和重连的常用命令有:tmux detach-client(或前缀键加 d)、tmux attach -t api、在另一个会话里用 tmux switch-client -t web 切换、以及 tmux kill-session -t api。

各种情况下的存活情况如下:

| 事件 | 智能体会继续运行吗? | | --- | --- | | 关掉终端窗口 | 是 | | 分离客户端 | 是 | | SSH 连接断开 | 是 | | tmux kill-session | 否 | | tmux kill-server | 否 | | 重启机器 | 否 | | macOS 重启后“重新打开窗口” | 否 |

tmux 服务端只是你用户名下的一个进程,它本身并不能扛住重启;服务端停止时,会话的回滚缓冲也会丢失。如果对话记录重要,就用 pipe-pane 存到文件,或者用能自己持久化历史的工具。

一个容易踩的细节是:分离状态的面板尺寸,取决于最后一个连上来的客户端,如果从来没人连过就是 80x24。你离开时智能体 TUI 在 80 列里渲染,等你用 200 列重新连上,在它重绘之前画面是乱的。若有多个不同尺寸的客户端,set-window-option -g aggressive-resize on 能缓解这个问题。

通过 SSH 使用 tmux

在远程机器上通过不信任的链路跑智能体时,tmux 很难被替代: ssh devbox -t tmux new-session -A -s agents 这条命令会分配 tty 并附着或创建会话。任务中途连接断了,再跑一次同样的命令,你就会回到同一个会话,智能体还在继续。如果链路差到 SSH 本身老是掉线,可以配合 mosh 使用。

嵌套 tmux 的问题:如果你本地和远程都跑了 tmux,两者会抢同一个前缀键。两种解法:给远程换个前缀(set -g prefix C-a),或者用 bind C-b send-prefix 支持连按两次前缀来把前缀发到内层。

关于在不是面前的机器上运行智能体,见 remote development。

tmux 的边界在哪里

tmux 是好软件,从 2007 年就开始把多路复用和持久化做到了近乎极致。但它没有为智能体做任何专门的设计,因为它的模型诞生时,智能体还不存在。

看不出哪个智能体卡住了。 等待审批的提示、正在思考、二十分钟前就已结束,这三种状态在 tmux 里长得一模一样,都只是“一个有几行文字的面板”。前面那个状态栏脚本就是用来打补丁的,而它靠的是抓屏。

结束时没有通知。 最接近的是 monitor-silence,但它对任何原因的安静面板都会触发。tmux 没有“智能体完成了”这种事件,因为它根本不知道智能体是什么。

抓屏本质上很脆弱。 capture-pane 给的是渲染后的画面:spinner、框线、截断的列、ANSI 码。它不包含智能体说了什么、调了哪个工具、调用是否成功。只要智能体改了 UI,你写的解析逻辑就会崩。

会话之间没有共享状态。 两个 tmux 会话里的两个智能体无法互相看见。想让一个读另一个的记录或交接任务,你得自己用文件、锁和轮询来实现。

多个流式输出的智能体,回滚缓冲很吃资源。 十几个面板都设了 history-limit 100000,在服务端存活期间会一直占着可观的内存,而服务端一停就全没了。渲染端的代价见 terminal emulators for AI agents。

重启后什么都不剩。 所有会话、所有对话记录都会消失。

这些都不是缺陷,而是一个为“一个人、一个键盘、复用 shell”设计的工具的能力边界。

crystl 接手的地方

crystl 是一款 macOS 终端,专为 tmux 原本不适用的那种负载而设计:同时跑很多智能体,大部分不在屏幕上,每个偶尔都需要你处理。

它的改进几乎一一对应上面那张清单:

  • 审批会浮出来,而不是被埋掉。 任何智能体暂停等权限时,crystl 会把它做成一张卡片浮起,按项目配色,你不用切会话就能回复,不依赖抓屏和轮询。详见 floating cards。
  • 智能体状态是一等信号。 agent activity panel 能同时显示所有会话的状态:工作中、等你、空闲、已完成。这正是前面状态栏脚本想近似模拟的东西。
  • 输出是结构化的,不是抓屏来的。 crystl 把每一轮对话、每次工具调用和结果捕获为带类型的记录,因此 history 可以跨会话搜索,智能体之间也能直接读彼此的记录,不用解析 ANSI。
  • 会话按项目组织,并且能持久化。 项目是 gem,里面的会话是 shard,关掉窗口或重启 Mac 后历史依然保留。详见 multitasking。
  • 同一仓库上的并行工作是隔离的。 隔离的 shard 背后是真正的 git 工作树,所以同一仓库上的两个智能体不会互相冲突。详见 parallel sessions。

建议是:保留 tmux。对于远程机器、服务器上的长跑进程,以及 SSH 链路比较脆弱的场景,它依然是正确的选择,该用就用。但当你管理的是六个智能体,且每个都会在不可预测的时刻等你做决定时,问题就已经不是多路复用了。

评论

讨论区几乎成了开发者自研 agent 终端编排工具的展示场,大量人都在分享自己手搓的类似工具;分歧主轴在于该继续用 tmux 等通用终端复用器,还是转向 herdr、Orca 等为 agent 优化的新工具,同时也有人对原文动机和底层架构提出质疑。

认为专用 agent 工具或自研终端体验更好

  • 许多人在尝试后认为,为 agent 编码专门优化的工具(如 herdr)开箱即用体验远好于手工配置 tmux,能更直观地展示和管理打开的任务;也有人转向 Orca IDE 后感觉可用性飞跃,或干脆用 Cursor 多窗口靠标题区分项目,甚至有人因为厌倦折腾 tmux 而彻底回到 ChatGPT 桌面应用,也有开发者自研原生终端。(较多人) > 自己的经验里,herdr 在面向 agent 的编码上开箱即用好得多,没发现什么是 tmux 能做而它做不到的(@thomas_witt)

认为 tmux 仍是可靠基础,但应厘清适用边界

  • tmux(或 screen)在远程服务器无法安装其他软件、或只需要基础持久化时依然是首选,甚至被视作开发者值得花十分钟掌握的基本功;但在本地可控环境下,也有人主张抛弃它这类「带内多路复用器」,改用专门工具做持久化(如 zmx),而在图形界面终端里用标签页或虚拟桌面做多路复用。(较多人) > 花十分钟学会 tmux 的基本快捷键,会是投入产出比极高的投资,对开发者而言如同 Vim 或 Bash 一样属于基本功(@hatradiowigwam)

认为把 agent 塞进 tmux 是错误架构,应该跑成守护进程

  • 从根本上说,编码 agent 应该作为无头或分离的守护进程运行;需要命令行时,让 CLI 去连接这个守护进程交互。这样就能随时从任意机器、用任意 UI 退出和恢复,而守护进程不关心;把 agent 作为交互式 CLI 进程再塞进 tmux,靠读取或刮取屏幕来「连接」,是很糟糕的做法。(少数人) > 编码 agent 本该是无头守护进程,让 CLI 去连接它;把它作为交互式进程塞进 tmux 并靠刮屏幕来交互,是错误架构(@teiferer)

认为类似的终端复用器已经泛滥,质疑重复造轮子的必要性

  • 不少人感叹顶层评论里几乎所有人都在分享自己手搓的类似工具,导致「上千个复用器」的局面;有人直接质疑,这到底解决了什么真实问题,以及沙箱隔离究竟该怎么做。(较多人) > 几乎每个顶层评论都在讲自己写了一个版本,让人好奇这到底解决了什么问题、又怎么做沙箱隔离(@nsagent)

关注 agent 的安全隔离与远程常驻执行

  • 比起纠结用哪个复用器,部分人更在意安全和环境问题:有人用 Mac 的安全隔区加指纹保护 SSH key,避免 agent 自己连远程机器;有人专门做沙箱、磁盘隔离和密钥加密;还有人把 agent 直接放服务器上,通过官方客户端远程控制,这样既能合盖继续跑、不烫笔记本,又能给 agent 任意配置环境和权限。(较多人) > 把 Claude Code 装在服务器上再用桌面端远程控制,任务留在服务器跑,既能合上笔记本继续工作,又不会把大腿烤熟(@blfr)

认为原文是广告且内容不可信

  • 有人尖锐指出原文充满 AI 生成的废话且漏洞百出,本质上只是某个工具(crystl)的软文;也有人附和说这基本就是广告,而 herdr 其实能覆盖文章里描述的所有功能。(少数人) > 有人直言这就是一篇错漏百出的 AI 洗稿,本质上只是给某个工具打广告(@tjoff)

有价值的补充

  • 只需要会话持久化、不需要终端内多路复用时,shpool 这类只做分离和恢复的工具反而更契合需求,多路复用交给浏览器或图形界面的标签页、虚拟桌面即可(@klodolph 的实践)。
  • 不少自研编排工具都内置了相似特性:给会话分配 UUID 以便随时终止或恢复,长时间无活动后自动休眠以释放资源,通过网页端、移动端或 Tailscale 接入,并全程录制以便随时恢复(综合 @zackham、@nklmilojevic 等自研者的描述)。
  • 若用 Mac 的安全隔区加指纹管理 SSH key,可确保 agent 无法自行登录远程机器,必须由人先完成指纹认证,再把命名好的 tmux 会话交给它接管(@littlecranky67 的安全实践)。