起因
Claude Code 是个 TUI,输入输出绑在终端里。想让别的程序操控一个正在跑的会话,官方没给 SDK,得自己想办法。
翻了两个项目,都是拿 tmux 做地基:
- hermes agent 的
claude-codeskill —— NousResearch/hermes-agent,skill 路径skills/autonomous-ai-agents/claude-code/SKILL.md - ccbot —— six-ddc/ccbot
一个是让 agent 遥控 Claude,一个是让人从聊天软件遥控 Claude。角度很不一样,但底下都是 tmux。记一下为什么。
为什么先想到 tmux
遥控 TUI 有三个硬约束:
- 全屏 TUI,不能
echo prompt | claude管道喂 claude -p跑完就退,交互模式又没 API- 信任对话框、权限弹窗这些只出现在屏幕上,裸 PTY 不好稳定识别
tmux 一把解决,而且顺手多给了几样:
| 能力 | 用来干什么 |
|---|---|
new-session -d | 后台起 pty 容器跑 claude,不占前台 |
send-keys | 打字、按方向键、发 Enter |
capture-pane | 抓屏幕文本(可带 ANSI 色码) |
| daemon | session 脱离父进程,脚本挂了 claude 还在 |
tmux attach | 人工随时接管 |
| window/session | 多路复用,N 个 claude 就是 N 个 window |
前三条是"遥控"需要的,后三条是附送的——但后三条恰恰是 ccbot 那种"从手机切回桌面"场景的关键。
hermes skill:最小用法
这个 skill 只用到前四个原语,就让一个 agent 能起一个下属 Claude 干活:
# 起后台 session
tmux new-session -d -s claude-work -x 140 -y 40
# 跑 claude
tmux send-keys -t claude-work 'cd /path/to/project && claude' Enter
# 等 TUI 初始化,发第一个 prompt
sleep 5 && tmux send-keys -t claude-work 'Refactor the auth module' Enter
# 看它在干嘛
sleep 15 && tmux capture-pane -t claude-work -p -S -50
# 追问
tmux send-keys -t claude-work 'Now add unit tests' Enter
# 收工
tmux send-keys -t claude-work '/exit' Enter
tmux kill-session -t claude-work
就这一套命令,拼出"起进程、发命令、读结果、清理"的闭环,没写一行状态管理代码。
局限也明显:信息全从屏幕文本抠。终端滚动会丢内容,ANSI 色码和框线得当噪音过滤,想知道 tool 调了啥参数只能看 TUI 怎么渲染。糙,但跑得通。
ccbot:加一条 JSONL 旁路
ccbot 的目的是让你人不在电脑前也能继续操作本地 Claude——手机上发消息,bot 转成 send-keys 打进本地 tmux;claude 的回复再从 tmux 那头读出来推到手机。
作者自己在 README 里点破了和别家 bot 的区别:
It operates on tmux, not the Claude Code SDK. Your Claude Code process stays exactly where it is, in a tmux window on your machine.
其他 bot 走 SDK 起独立 API 会话,那会话跟你本地终端是两回事,手机聊完回不去。ccbot 走 tmux,手机聊完回家 tmux attach 接着敲,scrollback 都在。这一点完全依赖 tmux 的 daemon 架构,没这条没得玩。
三条通道,各干各的
ccbot 实际用到 tmux 三种能力,对应三个问题。
1. send-keys:输入通道
手机里发的消息最终都是一次键码注入打进 tmux window。期间还要处理 TUI 的小脾气:发文本和 Enter 之间得插几百毫秒,否则 Enter 会被当换行而不是提交;! 开头的 bash 模式也要先发 ! 等 TUI 切换好再发剩下。都是跟 TUI 输入缓冲斗争的脏活。
2. capture-pane + JSONL:获取输出
Claude Code 会把对话内容写到 JSONL 文件,ccbot 直接轮询文件拿对话记录,不经过 tmux。
但权限请求、计划确认、选项菜单、首次启动的信任提示这些东西只活在屏幕上,进不了 JSONL。原因也简单:这些弹窗是 TUI 在等你按键,人没按之前模型根本没动,自然没东西可记。光靠 JSONL 会卡在"claude 说要跑 bash"之后突然没动静。
所以 ccbot 另起一条 1 秒一次的轮询,用 capture-pane 扫屏识别弹窗,扫到就在手机上用可点按钮把选项发给你,你点一下 bot 再 send-keys 发对应数字回去。tmux 在这里既当观察端也当控制端,JSONL 做不到这件事。
3. daemon + attach:持久化
tmux session 生命周期独立于 ccbot 进程。bot 挂了、重启了,tmux 里 claude 还在跑,回家 tmux attach 看到的是完整的会话。“从手机切回桌面不丢上下文"就是这么来的。
一条核心映射:1 个会话入口 = 1 个 tmux window = 1 个 Claude session。
两个项目对 tmux 的使用对比
| 需求 | tmux 原语 | hermes skill | ccbot |
|---|---|---|---|
| 起进程 | new-session / new-window | 每任务一个 session | 每 topic 一个 window |
| 注入输入 | send-keys | 发 prompt、方向键 | 转发聊天消息、弹窗选项 |
| 读输出 | capture-pane | 唯一信息源 | 只用来识别弹窗和状态行 |
| 持久化 | daemon | 任务可后台长跑 | 核心功能 |
| 人工接管 | tmux attach | 调试用 | 核心功能 |
| 多路复用 | window / session | 多任务并发 | 多项目并行 |
hermes skill 只用前三项,ccbot 六项全用。但抽象是一样的:把 tmux 当"带 API 的终端 + 带持久化的进程管理”。
