跳到正文
Hacker News· ankitg12·· 8 小时前AI 评分38

用 tmux 管理 Claude Code 智能体:会话、send-keys 与能力边界

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

AI 导读

Claude Code 近期版本支持用 `claude --worktree --tmux` 一条命令为单个分支任务创建 git worktree 和 tmux 会话,在 iTerm2 上默认使用原生分屏,需加 `--tmux=classic` 才走真实 tmux。

正文 · AI 翻译

tmux 将你的 AI 编码代理运行在具名、可分离的会话中,这些会话在终端关闭或 SSH 连接断开后依然存活。你得到一个窗口容纳多个代理,send-keys 可以从脚本向其中任意一个输入,capture-pane 可以读回其中一个。正是这种组合让人们使用它。

在你手动构建任何东西之前,先检查内置路径:较新版本的 Claude Code 可以用 claude --worktree --tmux 为你创建工作树和 tmux 会话。本指南先介绍这一点,然后介绍它未覆盖的所有内容,最后介绍 tmux 本身停止的地方。

为什么 tmux 和代理如此搭配

编码代理是一个长期运行的进程,它希望持续与终端对话。tmux 的三个特性与这种形态相匹配:

它能在断开连接后存活。 tmux 服务器拥有 pty,而不是你的终端窗口。关闭窗口、丢失 SSH 链接、让笔记本电脑休眠:代理继续工作,当你重新连接时它仍然在那里。

它在一个窗口中容纳多个会话。 四个代理变成四个具名会话,你可以用一次按键在它们之间切换,而不是在应用切换器中寻找四个窗口。

它可以从外部脚本化。 任何地方的任何 shell 都可以向正在运行的代理输入或读取其屏幕。无需构建 IPC,无需连接 API。这是人们构建的基础特性,也是 tmux 不断出现在代理编排脚本中的原因。

内置标志,在你构建任何东西之前

Claude Code 为单代理单分支场景内置了这一功能。在较新版本中,从 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.

因此整个“隔离此任务,给它自己的会话”操作就是一条命令:

claude --worktree --tmux

# or name the worktree
claude --worktree auth-refactor --tmux

在依赖它之前有两个细节很重要。--tmux 在没有 --worktree 时会报错,所以它是一个恰好会创建会话的工作树功能,而不是通用的会话标志。在 iTerm2 上它使用原生窗格而不是 tmux,这看起来更美观,但也意味着本页其余部分的 tmux 命令没有可附加的对象。当你想要真正的 tmux 底层时传入 --tmux=classic,如果你计划对会话进行脚本化,就需要这样做。

对于单分支上的单个代理,这就是答案。使用它并跳过后续内容。

它不做的事情:在一个会话中运行多个代理、布置窗格以便观察它们、让你从脚本向正在运行的代理输入、告诉你哪个代理在等你、或通过 SSH 在远程机器上设置这些。这些是本指南的其余部分,每一项都是人们最终手写 tmux 的地方。

关于该标志工作树部分的更多内容,请参阅 用于并行开发的 git worktrees。

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

保持可用的组织规则:每个仓库一个 tmux 会话,以仓库命名。不是按任务,不是按代理。任务来来去去;项目是稳定的东西。

# 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 是附加或创建。它使单条命令可以安全地重复运行,这正是你在 shell 函数中想要的:

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

会话名称不能包含 . 或 :(tmux 将它们用作目标分隔符),因此使用 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)

用格式字符串添加更多信息:

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

对于你不主动观察的代理,窗口比窗格更好,因为窗口不占用屏幕空间。窗格用于你现在想并排查看的两三个:

# 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} 很容易被遗漏。没有它,新窗格会在会话启动的目录中打开,而不是你实际所在的目录,你在那里启动的代理会指向错误的仓库。

两种按名称区分的布局:tiled 用于均匀的 agent 网格,main-vertical 用于一个大窗格(你正在驱动的 agent),其余窗格堆叠在旁边。

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 会让你想要做出的改动。

# 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 TUI 喜欢设置终端标题,所以如果没有 allow-rename off,你精心命名的 auth 窗口就会变成 agent 想给自己起的任何名字。

escape-time 10 解决了“在 agent 里我的方向键很卡”的抱怨。默认延迟的存在是为了区分真正的 Escape 键和转义序列,它让交互式 TUI 感觉黏糊糊的。

用 send-keys 驱动 agent

send-keys 像你坐在键盘前一样向窗格输入。这就是你从 cron、从 git hook 或从另一个 agent 脚本化驱动 agent 的方式。

# target syntax: session:window.pane
tmux send-keys -t api:auth "run the test suite and fix what fails" Enter

这里有三件事会出错,按发生频率排序:

裸参数会被解释为键名。 Enter、C-c、Escape 和 Space 是键,不是文本。如果你的提示词把其中某个词作为独立参数包含在内,它就会被当作按键发送。使用 -l 发送字面字符串:

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 输入框在粘贴时会防抖或重新渲染,与文本同一帧到达的 Enter 会被吞掉。可靠的模式:

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

中断失控的 agent 也是同样的机制:

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

用 capture-pane 读回 agent

capture-pane 将窗格内容转储到 stdout。它是这对操作中的读取一半。

# 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 比看起来更重要。没有它,在你的终端宽度处换行的行会以两行返回,你针对输出写的每个 grep 都会在接缝处出错。

要获得连续日志而非快照,pipe-pane 将窗格产生的所有内容流式传输到命令:

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

# stop it
tmux pipe-pane -t api:auth

-o 是切换式的,所以运行两次会将其关闭。输出是原始的,包含转义序列,所以如果你以后想让它可读,就通过类似 sed -E 's/\x1b\[[0-9;]*[a-zA-Z]//g' 的东西管道处理。

状态栏监视器

每个在 tmux 中运行多个 agent 的人最终都会构建这个:一个状态栏,告诉你哪些 agent 在工作、哪些在等待。

机制是在状态栏中使用 #(),它会运行一个 shell 命令并插入其输出,每 status-interval 秒刷新一次。

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

以及脚本,它通过 capture 抓取每个窗格以寻找生命迹象:

#!/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"

它能工作,也值得构建。要清醒地认识到它在做什么:对 TUI 渲染帧进行字符串匹配,每五秒轮询一次,针对你打开的每个窗格。匹配字符串是你的 agent 版本这周碰巧打印的任何内容。

更轻量的版本使用 tmux 自己的活动跟踪而不是抓取。monitor-silence 在窗格 N 秒没有输出时触发警报,这是最接近“这个 agent 停了”的原生信号:

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\\\"\""'

沉默是完成的代理,而不是完成的报告。一个正在认真思考大文件的 agent 也是沉默的。

分离、重新连接,以及什么能在什么之后存活

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

能存活的:

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

tmux 服务器只是属于你的用户的一个进程。tmux 本身没有任何机制能让它在重启后存活,而服务器停止时,会话的回滚缓冲区就会丢失。如果记录很重要,就把它pipe-pane到文件,或者使用一个能自行持久保存历史的工具。

有一个细节容易让人踩坑:分离会话的窗格尺寸会保持为最后一个连接客户端的大小,如果从未有过客户端,则为 80x24。一个 agent TUI 在你离开时渲染到 80 列的窗格中,当你以 200 列重新连接时会看起来不对,直到它重绘为止。当多个客户端以不同尺寸连接时,set-window-option -g aggressive-resize on会有帮助。

通过 SSH 使用 tmux

这正是 tmux 真正难以被替代的地方。在远程机器上、通过你不信任的链路运行 agent:

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

你的连接在任务中途断开,你再次运行同一条命令,就回到了同一个会话,agent 仍在继续运行。如果你的链路差到 SSH 本身都不断断开,就把它和mosh搭配使用。

嵌套 tmux 的问题:如果你在本地运行 tmux,同时在远程机器上也运行 tmux,两者会抢占同一个前缀键。两种解决办法是:在远程使用不同的前缀键(set -g prefix C-a),或者按两次前缀键,bind C-b send-prefix 就能给你这个效果。

要在不是你面前这台机器的机器上运行 agent,请参阅远程开发。

tmux 的局限

tmux 是好软件。它把多路复用和持久化解决得几乎不能再好了,而且从 2007 年起就一直在做这件事。它没有做的,是任何专门针对 agent 的事情,因为它的模型被设计出来时 agent 还不存在。

无法看到哪个 agent 处于等待状态。一个在等待审批提示的 agent,看起来和一个正在思考的 agent 一模一样,也和一个二十分钟前就已完成任务的 agent 一模一样。这三者都是“一个里面有些文字的窗格”。上面提到的状态栏 hack 就是为了掩盖这一点,而它靠的是抓取屏幕内容。

某个 agent 完成时没有通知。monitor-silence 是最接近的东西,但它会因为任何原因在任何安静的窗格上触发。没有“agent 已完成”这样的事件,因为 tmux 根本不知道 agent 是什么。

屏幕抓取在构造上就是脆弱的。capture-pane 给你的是渲染后的画面:旋转指示器、制表符、被截断的列、ANSI。不是 agent 说了什么,不是它调用了哪个工具,也不是那次调用是否成功。你针对它写的每一个解析器,都会在 agent 发布 UI 变更时失效。

跨会话没有共享状态。两个 tmux 会话中的两个 agent 无法看到彼此。如果你想让一个读取另一个的记录或交接工作,你得自己用文件、锁和轮询来构建。

为多个流式 agent 保留回滚缓冲区代价高昂。在十几个 agent 输出窗格上使用history-limit 100000 是实打实的内存,只要服务器还活着就按窗格持有,服务器一停就消失。关于为什么渲染这一侧也会变得昂贵,请参阅面向 AI agent 的终端模拟器。

重启后什么都不保留。每个会话、每份记录,全部消失。

这些都不是缺陷。它们是一个为单个键盘前的人多路复用 shell 而构建的工具的边界。

crystl 从何处接续

crystl 是一个为 tmux 未曾针对的工作负载而构建的 macOS 终端:同时运行许多 agent,其中大多数在屏幕之外,每一个偶尔需要你。

这些差异几乎一一对应上面的清单。

  • 审批会浮出水面,而不是被埋没。当任何 agent 暂停等待许可时,crystl 会将其以卡片形式浮出,按项目进行颜色编码,你无需切换会话即可作答。无需抓取,无需轮询。参见浮动卡片。
  • Agent 状态是一等信号。agent 活动面板会同时显示每个会话的状态:工作中、等待你、空闲、完成。这正是状态栏脚本此前试图近似实现的东西。
  • 输出是结构化的,而非抓取而来。crystl 将每一轮、每次工具调用和结果都捕获为带类型的记录,因此历史可在所有会话中搜索,agent 之间也能互相读取转录内容,而无需解析 ANSI。
  • 会话按项目组织,并且会持久保留。项目是宝石,其中的会话是碎片,历史在关闭窗口或重启 Mac 后依然存在。更多内容见多任务。
  • 同一仓库上的并行工作是隔离的。隔离的碎片由真实的 git worktree 支撑,因此同一仓库上的两个 agent 不会冲突。参见并行会话。

建议:保留 tmux。对于远程机器、服务器上的长时间运行进程,以及任何 SSH 链路是脆弱环节的场景,它仍然是正确答案。就用于这些场景。当你管理的是六个 agent,而每个 agent 都会在不可预测的时刻需要你做出决定时,问题就不再是多路复用了。

关于这方面的组织侧内容,更多见管理多个 AI 编码会话;关于让 agent 保持工作而非空闲,见让 AI 编码 agent 运行更久。

来源:Hacker News · crystl.dev