cmux 一键复刻配置
cmux(Ghostty 内核的 macOS AI 终端)的配色、侧栏、远程 SSH 全套配置,附一次实配过程中踩到的 12 个坑与实测数据。
在一台新 macOS 机器上复刻 cmux 环境:纯白内容卡 + 暖杏侧栏、agent 休眠、原生 SSH 工作区(标题跟随远端目录)。
2026-08-05 变更:这份配置不再用侧栏分组。 折叠加嵌套之后侧栏反而更难扫,改成一行一个会话的平铺列表。§1 的复刻块已经去掉
workspaceGroups和建组步骤,两个建组快捷键(⌃⌘G/⌘⇧G)都解绑了。分组的机制说明和实测坑留在 §2 和 §3.4 备查 —— 那些结论对 cmux 本身仍然成立,只是这套配置不用了。
用法:把 §1 的整块复制粘给 Claude Code。它自包含——所有配置全文和约束都在块内,不依赖本地文件。§2 之后是给人看的参考,复刻时不必读,但 §3 的坑清单值得先扫一遍,那是这套配置里唯一无法从文档推导出来的部分。
适用环境:macOS (Apple Silicon),cmux 装在
/Applications/cmux.app,14” 屏(部分选择是为窄屏做的,见 §2)。 配置快照:2026-08-01,cmux 0.64.20 (build 100)。 同一台机器的 shell 侧配置见 Zsh 一键复刻配置,两份合起来才是完整环境。
1. 一键复刻 Prompt(唯一入口,整块复制)
你是我的环境配置助手。请在这台 macOS (Apple Silicon) 机器上复刻我的 cmux 配置。
所需信息全在本提示词内,不要去读任何本地文件。
【总约束】
1. 动手前备份:~/.config/cmux/cmux.json 和 ~/.config/ghostty/config 若已存在,
各复制一份为 *.bak.<今天日期时分秒>,绝不覆盖未备份的文件。
2. 配置写 ~/.config/cmux/cmux.json(primary)。~/.config/cmux/settings.json 是 legacy,
不要往里写,两个文件都有同名键会打架。用 `cmux config paths` 可确认地位。
3. 改完执行 `cmux reload-config` 生效(同时重载 cmux + ghostty 配置,不需要重启 app)。
校验用 `cmux config doctor`。
4. cmux 自带 ghostty CLI,路径 /Applications/cmux.app/Contents/Resources/bin/ghostty,
可用 `+list-themes` 预览主题。cmux 命令在 /Applications/cmux.app/Contents/Resources/bin/cmux。
5. cmux.json 是 JSONC(允许注释和尾逗号)。但 `cmux config doctor` 对尾逗号宽容,
容易漏;写完请额外用严格 JSON 解析器验一遍(把 // 注释行剥掉再 json.loads)。
【第一步:~/.config/ghostty/config】
终端配色/字体/内边距/起始目录由 ghostty 配置控制,cmux 直接读它。写入以下内容:
# —— 日夜自动切换 ——
theme = light:GitHub Light Default,dark:GitHub Dark Dimmed
# —— 新工作区/窗口的起始目录 ——
# 生效前提:cmux.json 里 app.workspaceInheritWorkingDirectory = false
# 可填绝对路径 / ~/xxx / home / inherit
working-directory = ~/Documents/_work/99_code
# —— 内边距留白,配合悬浮卡片观感 ——
window-padding-x = 12
window-padding-y = 10
window-padding-balance = true
# background-opacity 保持默认 1(不透明)。别试透明毛玻璃,原因见下方说明。
【第二步:~/.config/cmux/cmux.json】
写入以下内容(注释保留,它们记录了为什么这么选):
{
"$schema": "https://raw.githubusercontent.com/manaflow-ai/cmux/main/web/data/cmux.schema.json",
"schemaVersion": 1,
"app": {
"appearance": "system",
"minimalMode": true,
"workspaceInheritWorkingDirectory": true,
"forkConversationDefaultDestination": "newTab",
"globalFontMagnification": 90,
"hideTabCloseButton": true,
"reorderOnNotification": true,
"warnBeforeQuit": true
},
"activePaneBorderColor": "#D97706",
"sidebarAppearance": {
"matchTerminalBackground": false,
"lightModeTintColor": "#E8955A",
"darkModeTintColor": "#E8B48A",
"tintOpacity": 0.11
},
"workspaceColors": {
"indicatorStyle": "lift",
"selectionColor": "#FDF8F3"
},
"canvas": { "paneGap": 20 },
"sidebar": {
"showProgress": true,
"showLog": true,
"showPorts": true,
"showPullRequests": true,
"showSSH": true,
"showBranchDirectory": false,
"showNotificationMessage": true,
"notificationMessageLineLimit": 3,
"beta": {
"workspaceTodos": { "checklistStyle": "inline" }
}
},
"markdown": {
"fontSize": 16,
"maxWidth": 820,
"fontFamily": ""
},
"fileEditor": { "wordWrap": true },
"fileExplorer": { "doubleClickAction": "preview" },
// 不用侧栏分组,所以没有 workspaceGroups 块(它只管分组头的图标/颜色/落位)。
// 两个建组快捷键都解绑,免得侧栏又冒出组来。cmux 没有"禁用分组"的开关,
// 解绑是能做到的最接近的事;右键菜单和 CLI 不受影响。
"shortcuts": {
"bindings": {
"newWorkspaceGroup": null,
"groupSelectedWorkspaces": null
}
},
"terminal": {
"agentHibernation": {
"enabled": true,
"idleSeconds": 30,
"maxLiveTerminals": 6
},
"uploadCommands": [
{
"command": "scp -q -o ControlMaster=auto -o ControlPath=/tmp/cmux-ssh-%r@%h:%p -o ControlPersist=300 ${CMUX_UPLOAD_PORT:+-P $CMUX_UPLOAD_PORT} ${CMUX_UPLOAD_IDENTITY_FILE:+-i $CMUX_UPLOAD_IDENTITY_FILE} $CMUX_UPLOAD_SSH_OPTIONS \"$CMUX_UPLOAD_LOCAL_PATH\" \"$CMUX_UPLOAD_DESTINATION:$CMUX_UPLOAD_REMOTE_PATH\" >&2 && printf %s \"$CMUX_UPLOAD_REMOTE_PATH\"",
"enabled": true
}
]
},
"automation": {
"workspaceAutoNaming": true,
"autoNamingAgent": "auto"
},
"notifications": {
"dockBadge": true,
"showInMenuBar": true,
"unreadPaneRing": true,
"paneFlash": true,
"agentPermissionPrompt": true,
"agentTurnComplete": "whenIdle",
"agentIdleReminder": true,
"suppressOnlyFocusedSurface": true,
"sound": "Tink"
},
"diffViewer": { "defaultLayout": "unified" },
"commands": [
{
"name": "新工作区 · 99_code",
"description": "从代码根目录开一个空工作区(显式 --cwd 才可靠,见 §5)",
"keywords": ["new", "workspace", "99code", "root"],
"command": "cmux workspace create --cwd ~/Documents/_work/99_code"
},
{
"name": "SSH · dev-env",
"description": "标题跟随远端目录",
"keywords": ["ssh", "devenv"],
"command": "cmux ssh <user>@<dev-host> --identity ~/.ssh/id_ed25519"
},
{
"name": "SSH · agent-env",
"description": "标题跟随远端目录",
"keywords": ["ssh", "agentenv"],
"command": "cmux ssh <user>@<agent-host> --identity ~/.ssh/id_ed25519"
},
{
"name": "SSH · 重连断掉的远程会话",
"description": "只重连不健康的远程工作区(合盖醒来用;见 §3.12)",
"keywords": ["ssh", "reconnect", "wake"],
"command": "zsh -ic sshreconnect"
},
{
"name": "SSH · 强制重连全部远程会话",
"description": "无条件重连所有远程工作区",
"keywords": ["ssh", "reconnect", "force"],
"command": "zsh -ic 'sshreconnect -f'"
}
]
}
把 commands 里的 <user>/<dev-host>/<agent-host> 和 --cwd 路径换成本机实际值。
重连预设调用的 sshreconnect 函数在第三步的 ~/.zshrc 里,逻辑只维护这一份。
【第三步:~/.zshrc 追加】
# ---- cmux diff 快捷方式 ----
alias cdiff='cmux diff --unstaged' # 未暂存改动
alias cdlast='cmux diff --last-turn' # agent 这一轮改了什么
alias cdbr='cmux diff --branch' # 当前分支 vs merge base
# ---- cmux 原生 SSH 工作区 ----
# 故意不传 --name:显式命名会永久盖住远端发来的 OSC 标题(见 §3.6)。
# 侧栏里认哪台机器,看工作区下面的 SSH 明细行(sidebar.showSSH)。
devenv() { cmux ssh <user>@<dev-host> --identity "$HOME/.ssh/id_ed25519" "$@"; }
agentenv() { cmux ssh <user>@<agent-host> --identity "$HOME/.ssh/id_ed25519" "$@"; }
# ---- 一键重连远程工作区(合盖过夜后救活,见 §3.12)----
# 默认只重连不健康的(跳过正常连接,不打断正在跑的 agent);-f 强制全部。
# 实测重连不丢会话(远端 session id 前后一致)。判活用 state+daemon+proxy,
# 不看 remote.heartbeat(那字段会冻住不刷新,见 §3.12)。
sshreconnect() {
local cmux; cmux=$(command -v cmux) || { echo "找不到 cmux CLI"; return 1; }
local force=""; [ "$1" = "-f" ] && force="1"
"$cmux" list-windows --json 2>/dev/null | FORCE="$force" python3 -c '
import sys, json, os, subprocess
force = os.environ.get("FORCE") == "1"
cmux = "cmux"
try: wins = json.load(sys.stdin).get("windows", [])
except Exception: wins = [{"ref": None}]
refs = [w.get("ref") for w in wins] or [None]
targets, skipped = [], 0
for wref in refs:
cmd = [cmux, "workspace", "list", "--json"] + (["--window", wref] if wref else [])
try: d = json.loads(subprocess.run(cmd, capture_output=True, text=True).stdout)
except Exception: continue
for w in d.get("workspaces", []):
r = w.get("remote", {})
if not r.get("enabled"): continue
healthy = (r.get("state")"connected" and r.get("daemon",{}).get("state")"ready"
and r.get("proxy",{}).get("state")=="ready")
if healthy and not force: skipped += 1; continue
targets.append((wref, w["ref"], r.get("destination") or "?", r.get("state")))
if not targets:
print(f" 没有需要重连的(跳过 {skipped} 个健康连接)" if skipped else " 没有远程工作区"); sys.exit(0)
for wref, ws, dest, state in targets:
cmd = [cmux, "workspace", "reconnect", "--workspace", ws] + (["--window", wref] if wref else [])
ok = subprocess.run(cmd, capture_output=True, text=True)
print(f" {\"OK\" if ok.returncode==0 else \"失败\"} {ws} {dest} (原 state={state})")
if skipped: print(f" (另跳过 {skipped} 个健康连接,-f 可强制全部)")
'
}
【第四步:远端 daemon(国内网络必做,否则 cmux ssh 一定超时)】
cmux ssh 要在远端跑 cmuxd-remote daemon,这个二进制不在 app 包里,要从 GitHub Releases 下。
国内直连 release-assets.githubusercontent.com 会超时,报
"Remote daemon bootstrap failed: 请求超时"。做法:
a) 查目标平台的资源名、下载 URL、期望 sha256、缓存路径:
cmux remote-daemon-status --os linux --arch amd64
b) 用能翻的网络(浏览器/代理)下载那个 cmuxd-remote-linux-amd64。
c) 放到 ~/.local/state/cmux/remote-daemons/<版本号>/linux-amd64/cmuxd-remote 并 chmod +x。
d) 再跑一次 a),应显示 cache exists: yes / cache verified: yes。
【重要】下载后必须核对 sha256,不能只看 HTTP 200。实测同一条内网链路上
366B 的 checksums.txt 完好,而 5.9MB 的二进制被截断成 2.7MB 却仍返回 200。
macOS `file` 报 "too large section header offset" 就是截断征兆。
缓存路径带版本号,cmux 升级后失效,同样的超时会再犯一次。
【第五步:远端 shell 的标题联动(可选但推荐)】
让 cmux 侧栏标题跟着远端当前目录实时变。先确认远端交互 shell 是 bash 还是 zsh
(注意:登录 shell 可能是 bash,但 ~/.bashrc 里有 exec zsh 把它换掉,
这种情况要改 ~/.zshrc,往 .bashrc 末尾加东西永远执行不到)。
zsh 版本,追加到远端 ~/.zshrc 末尾(必须在 oh-my-zsh / powerlevel10k 之后,
否则会被它们的标题设置盖掉):
# >>> BEGIN cmux-title (整段删除即可还原)
: ${CMUX_HOST_LABEL:=dev-env}
__cmux_set_title() {
printf '\033]0;%s:%s\007' "$CMUX_HOST_LABEL" "${PWD/#$HOME/~}"
}
autoload -Uz add-zsh-hook
add-zsh-hook precmd __cmux_set_title
# <<< END cmux-title
bash 版本用 PROMPT_COMMAND 前插,并加 [ -n "$PS1" ] 守卫避免污染非交互会话。
改远端配置前先备份,且用标记注释包裹以便整段删除。
【第六步:验证】
cmux config doctor # 两个配置文件都应 OK,keys 里不该有 workspaceGroups
cmux reload-config # 应返回 OK Reloaded config
cmux themes list # 确认 light/dark 主题名生效
cmux workspace-group list # 应返回 No groups —— 这套配置不用分组
source ~/.zshrc && devenv # 开远程工作区,侧栏应显示主机 + "已连接"
日常一条要记住:笔记本过夜合盖后,SSH 工作区可能卡在"connected 但传输已断",
不会自愈(详见 §3.12)。救活是一条命令,远端会话零损失:
cmux workspace reconnect --workspace <ws>
【完成后告诉我】
- 侧栏是暖杏底 + 纯白内容卡 + 选中项浮起吗
- devenv 连上后,在远端 cd 时侧栏标题跟着变吗
2. 配置分区说明
配置分两个文件,边界很清楚:
| 归属 | 文件 | 管什么 |
|---|---|---|
| 终端渲染 | ~/.config/ghostty/config | 主题、字体、内边距、透明度、起始目录 |
| cmux 自身 | ~/.config/cmux/cmux.json | 侧栏、图标、通知、agent 行为、SSH 预设 |
配色:纯白内容卡 + 暖杏侧栏
内容区用 GitHub Light Default(纯白),侧栏叠一层暖杏 tint(#E8955A @ 11%,实际渲染约 #F1E8E6)。选中项用 indicatorStyle: lift + selectionColor: #FDF8F3(暖象牙白)——比侧栏亮所以”浮起”效果成立,比纯白柔和所以跟暖杏同色系更搭。
强调色 #D97706(琥珀)只用在活动面板边框,且只在分屏时可见。
为窄屏做的六个选择
14” 屏上这几项和大屏的最优解相反:
app.minimalMode: true—— 隐藏工作区标题栏,省一行垂直空间app.globalFontMagnification: 90—— UI 整体缩到 90%(终端、标签、侧栏、浮层;不影响浏览器里渲染的网页)app.forkConversationDefaultDestination: newTab—— Fork Conversation 默认去新 tab 而不是右侧分屏。右键 tab 的子菜单仍可选任意方向diffViewer.defaultLayout: unified—— 左右分栏在窄屏每侧太窄,代码全折行反而难读sidebar.notificationMessageLineLimit: 3—— 默认 12 行,一个工作区能吃掉侧栏 1/5 高度app.hideTabCloseButton: true—— 单 surface 时那条横向标签栏没法隐藏,只能去掉 ✕ 减少噪音
窄屏上与其分屏,不如用 ⌘B 隐藏侧栏、⌘T 开新 tab(每个都是全宽)、⌘⇧回车 临时全屏当前面板。
用法:一行一个会话,只走纵向
cmux 的四层模型是 Window / Workspace / Pane / Surface。关键点:侧栏的一行就是一个 workspace,没有比它更轻的”会话”对象——cmux 内部就把侧栏这些行叫 tab(cmux sidebar-state 返回的字段名是 tab=<uuid>),所谓 “vertical tabs” 指的正是它们。
所以”一个工作区放一个会话、在侧栏纵向往下加”是正统用法,不是绕路:
⌘N(newTab)→ 新建侧栏一行⌘T(newSurface)→ 在当前工作区内加一个横向 tab —— 不想要横向就别用它⌘W关 tab。app.keepWorkspaceOpenWhenClosingLastSurface保持默认false,关掉最后一个 surface 会连整行一起收掉,不留空壳
单 surface 时那条横向 surface 标签栏没有配置项能隐藏(搜过整个 schema)。只有两个相关开关:app.hideTabCloseButton(去掉关闭按钮,减少噪音,已开)和 ui.surfaceTabBar.buttons(能定制按钮,但是 nightly 特性)。minimalMode 隐藏的是工作区标题栏,不是这条。
侧栏分组(已不用,机制留档)
这套配置 2026-08-05 起不用分组了。 用过四天的结论:分组确实能把侧栏收短,但代价是多一层折叠——想找某个会话,先要记住它在哪个组、那个组是不是折着的。平铺列表虽然长,扫一眼就到底,反而更快。
下面的机制说明和 CLI 速查都还准(对 cmux 0.64.20 成立),留着备查。注意 cmux 没有”禁用分组”的开关:
workspaceGroups只管分组头的图标/颜色/落位,分组本身是侧栏内置行为。所以”关掉”只能是三件事:解散已有的组(ungroup,保留成员)、删掉workspaceGroups配置、解绑两个建组快捷键。
核心模型一句话:每个分组由一个 anchor 工作区拥有,anchor 那一行就是分组头,没有额外的头行。 点标题聚焦 anchor 的面板,点箭头折叠。
三个快捷键(前两个在这份配置里都已用 shortcuts.bindings 解绑):
| 快捷键 | 动作 | 触发条件 |
|---|---|---|
⌃⌘G | 新建空分组 | 无条件,误触就多一个叫「分组 N」的空壳 |
⌘⇧G | 把选中的工作区成组 | 必须选中 ≥2 个,否则不响应 |
⌃⌘. | 折叠/展开聚焦的分组 | 没有组时是空操作,所以没解绑 |
⌘⇧G 和 React Grab 撞键,cmux 只在有多选时才抢这个键,所以平时不受影响;解绑之后彻底让给 React Grab 了。真正容易误触的是 ⌃⌘G —— 无条件触发,误触过两次。
新工作区进不进组,UI 和 CLI 不一样(这条实测出来的,文档没写):
| 路径 | 结果 |
|---|---|
UI 里活动工作区是 anchor 或成员时按 ⌘N | 新工作区进组 |
分组头 hover 出来的 + 按钮 | 进组,cwd = anchor 的 cwd |
CLI cmux workspace create 不带 --group | 永不进组 |
CLI 那条试了两种可能的暗示方式——CMUX_WORKSPACE_ID 指到组内成员、以及当前选中项就在组内——都不进组。要进组必须显式 --group <g>(还有 --group-placement / --group-reference)。
cmux ssh 更彻底:它根本没有 --group 参数(只有 workspace create 有)。所以 devenv/agentenv 这类基于 cmux ssh 的函数开出来的远程工作区天生游离,不会进任何组,也没有”以后连 .130 的都归 dev-env”这种规则——分组只是一次性把当时的工作区收进壳。想让它自动进组,只能在函数里绕:cmux ssh 的 stdout 是 OK workspace=workspace:N target=... state=...,解析出这个 ref,再按组名(不是运行时会变的 workspace_group:N)动态查到 group ref,workspace-group add 进去;add 幂等所以重连也安全。§1 第三步的 devenv/agentenv 曾经这么干过,撤掉分组时一起删了 —— 现在就是直接 cmux ssh。
组内新建工作区落在哪由 workspaceGroups.newWorkspacePlacement 决定:afterCurrent(默认,插在活动成员后面)/ top(紧跟 anchor)/ end(追加到最后)。「一行一个会话往下加」的用法配 end 顺序最可预期。还能在某条 byCwd 规则里单独覆盖。
分组的 pin 独立于工作区的 pin,置顶的分组排在所有未置顶的顶层行之上。组名、anchor、pin、折叠状态、颜色、图标都跨重启保留,成员关系存在每个工作区上。
CLI 速查(<group> 收 UUID 或 list 打印的 workspace_group:N):
cmux workspace-group list --json # icon/color 为 null 才是走 byCwd
cmux workspace-group create --name x --from a,b # 永远显式 --from(见 §3.4)
cmux workspace-group create --name x --from "" # 真空组,不碰现有工作区
cmux workspace-group add --group <g> --workspace <ws> # 从别的组移过来也是这条
cmux workspace-group remove --workspace <ws>
cmux workspace-group set-anchor --group <g> --workspace <ws>
cmux workspace-group ungroup <g> # 解散,保留成员
cmux workspace-group delete <g> # 连带关闭所有成员,destructive
cmux workspace-group set-icon <g> --symbol server.rack # 传 "" 清除,退回 byCwd
cmux workspace-group set-color <g> --hex "#1A5276"
cmux workspace-group collapse|expand|pin|unpin|focus <g>
cmux workspace-group move <g> --to-index 0 # 也支持 --before / --after
cmux workspace-group new-workspace <g> [--placement end]
误关了工作区可以用 reopenClosedWorkspace 动作救回来。
byCwd 每条规则其实有四个字段,常用的只有前两个:icon、color、newWorkspacePlacement(单目录覆盖落位)、contextMenu(分组 + 按钮的右键菜单,schema 与 ui.newWorkspace.contextMenu 相同——后者标注为 nightly,这份配置没用)。
侧栏直接显示 agent todo
sidebar.beta.workspaceTodos.checklistStyle 有两个值,功能本身一直是开的、没有开关,只能选展示方式:
inline(现用)—— 清单就地在侧栏那行下面展开,扫一眼就知道 agent 在干到第几步popover—— 从摘要行弹锚定浮层,不占侧栏纵向空间
窄屏上 inline 会往下顶其它工作区,如果侧栏工作区多到挤不下,换回 popover。
markdown 查看器与文件树
内置 markdown 查看器带 live reload——⌘ 点 .md 打开后,文件被改(包括 agent 改)会自动刷新,写 wiki 类内容很顺。
markdown.fontSize: 16—— 默认 15,但开了 90% 缩放后要提一档正文才舒服。查看器内⌘+/⌘-/⌘0可临时缩放markdown.maxWidth: 820—— 默认 980 在 14” 上一行太长;中文一行 45 字左右最好读fileEditor.wordWrap: true—— 默认 false。中文长句没有空格,不换行就得横向滚动fileExplorer.doubleClickAction: preview—— 保持默认。cmux 内置预览支持 markdown live reload、PDF、图片,比丢给外部应用顺。想双击进外部编辑器要两处一起设:改成preferredEditor并设app.preferredEditor命令,只改前者会回落到 macOS 默认应用
agent 相关
agentHibernation(默认关):活跃 agent 终端超过maxLiveTerminals: 6时,后台空闲的自动休眠释放内存,回访时用保存的 session 恢复workspaceAutoNaming:auto模式下每个会话用它自己的 agent 起名(Claude 的用 Claude,Kiro 的用 Kiro)notifications.agentTurnComplete: whenIdle:后台任务真跑完才提示一次,不会中途误报notifications.suppressOnlyFocusedSurface: true:并行多 agent 时,非聚焦面板的横幅会留着等你看
SSH 上传走连接复用
terminal.uploadCommands 里那条规则替代内置 scp,加 ControlMaster=auto + ControlPersist=300。收益是拖 N 个文件只握手 1 次。不加 -C 压缩——传的是 .json.gz,已经压过。
命令里 scp 输出全转 stderr,stdout 只回远端路径,cmux 会把它插到光标处。想临时停用改 enabled: false。
3. 踩过的坑
这节是这份文档的主要价值。以下每条都是实测结论,不是文档推导。
3.1 配置文件:settings.json 是 legacy
~/.config/cmux/settings.json 是旧位置,primary 是 cmux.json。两个文件都放同名键会打架。用 cmux config paths 确认,cmux config doctor 校验。
顺带:cmux reload-config 一条命令重载 cmux + ghostty 两份配置,不需要重启 app。
3.2 「毛玻璃侧栏 + 不透明白卡」做不到
想复刻 otty 的 Floating Card 那种双层效果(外框半透明透出壁纸、中间白卡实心),在 cmux 0.64.20 上是死路,三个原因叠加:
sidebarAppearance只有实色 tint,cmux 没有独立的窗口 vibrancy/material 配置项- ghostty 的
background-opacity是全局的,一降透明度中间正文会跟侧栏一起被壁纸染色 - 想用
background-image单独把正文盖白也不行——background-image-opacity是相对background-opacity的、封顶
otty 能做是因为它的主题格式把 [sidebar] / [container] / [window] material 分开定义。所以只能在”白卡”和”通透”之间选一个,这份配置选了白卡(background-opacity = 1)。
3.3 selectionColor: null 不是”自适应中性”,是系统蓝
想让选中卡走中性色而把这个字段设 null,结果会回退到 cmux 默认的系统蓝。要白色悬浮卡必须显式写颜色。该字段不分明暗,只有一个值——所以纯白卡在深色模式下会偏亮扎眼。
3.4 分组这套东西,四个坑连在一起
workspaceGroups.byCwd 配的是侧栏分组头的图标和颜色。围绕它有四个独立的坑,实测逐个确认过。
这份配置现在不用分组了(见 §2),下面四条对 cmux 本身仍然成立,留档。第 (4) 条尤其值得记住 —— 它能在你只想建个组的时候悄悄关掉一个正在跑的 agent 会话。
(1) 没有分组时规则完全不显示。 分组必须显式创建,不会按 cwd 自动形成。看起来像配置没生效,其实是缺前提。分组是运行时状态,不进 cmux.json,换机器要重建。
(2) 匹配的是 anchor 的 cwd,不是成员的。 anchor 的 cwd 来源有三条:成组时继承 --from 里第一个成员 / CLI 不传 --cwd 时继承活动工作区 / create --cwd <path> 显式指定(实测有效,而且允许不存在的路径,会原样记下来)。
所以 cmux ssh 开的远程工作区是个死角——它们在本机视角 cwd 是空字符串,byCwd 永远匹配不上:
workspace:1 (本地 anchor) cwd='/Users/.../awp-aggregator' → 规则命中
workspace:11 (SSH 工作区) cwd='' → 任何规则都不命中
两条出路:set-icon/set-color 显式设(命令式,换机器丢),或者给远程组建一个 cwd 落在空标记目录里的本地 anchor(声明式,写在 cmux.json 里):
mkdir -p ~/Documents/_work/99_code/_remote/dev-env
cmux workspace-group create --name dev-env --from "" \
--cwd ~/Documents/_work/99_code/_remote/dev-env # --from "" = 真空组,不碰现有工作区
cmux workspace-group add --group workspace_group:N --workspace <ssh 工作区 ref>
这份配置原来走的是第二条(_remote/ 下两个空目录当 anchor),撤掉分组时连 anchor 工作区一起关了。
(3) 侧栏分组头显示的是 anchor 工作区的标题,不是组名。 这条最容易误判。workspace-group create --name X 会把组名和 anchor 标题一起设成 X,看起来一致;但之后 workspace-group rename 只改组名,不动 anchor 标题,侧栏毫无变化。实测:
$ cmux workspace-group rename workspace_group:5 --name zz-renamed
OK
$ cmux workspace list # anchor 标题没变
workspace:18 zz-test ← 侧栏显示的是这个
所以 ⌃⌘G 建出来的 分组 2 这类标题,改组名是白费的,得改工作区:
cmux workspace rename --workspace <anchor> "blogv2"
(4) create 不传 --from 会抓走你正在看的工作区。 CLI help 写的是 “Defaults —from to the active sidebar selection / caller workspace”——选中项优先于调用方。实测在一个 SSH agent 会话被选中时跑:
cmux workspace-group create --name zz-test --cwd ~/somewhere # 没传 --from
# → 新组成员是 [新 anchor, workspace:5],workspace:5 是正在跑 agent 的 SSH 会话,
# 而且它被从原来的 dev-env 组里拽出来了(一个工作区只能属于一个组)
危险在下一步:workspace-group delete 会连带关闭组内所有工作区。顺手 delete 就等于关掉那个活会话。永远显式传 --from;要真空组就 --from "";解散用 ungroup(保留成员),delete 才是 destructive 的。
3.5 工作区标题里的 session-id 后缀无法关掉
Claude Code 的工作区标题会带 · <session-id 前 16 字符> 后缀(如 · 78f819f5-9bcb-4f)。把整个 schema 搜遍了,没有任何开关能去掉它。来源是 cmux 读 Claude Code 的 session JSONL 文件名。
唯一可靠解是手动改名(⌘⇧R 或 cmux workspace-action --action rename),文档明确写了手动命名永久优先。
3.6 --name 会永久压死 OSC 标题
这条和 3.5 是同一个机制的两面。给 cmux ssh 传 --name 相当于手动命名,会把远端通过 OSC 转义序列发来的标题彻底盖住。所以要标题跟随远端目录,就不能传 --name,二者只能选一个。
3.7 远端命令没法自动执行
想让 cmux ssh 连上后自动跑一条远端命令(比如 zellij attach),两条路都不通:
| 写法 | 结果 |
|---|---|
--command "…" | 压根不执行(用远端标记文件验证过:工作区能连上,命令不跑) |
-- <远端命令> | 命令会执行,但绕过 cmux 远端 shell 集成 → 永远卡在 [ssh:connecting]、持久 PTY 建立不起来、转义序列漏成乱码(终端里出现 ^[[?997;2n) |
结论:连上后手敲。而且 cmux 自己的持久 PTY 已经做了 zellij 的保活那份活,新活儿不必再套一层。
3.8 远端 shell 可能不是 passwd 里那个
getent passwd 和 $SHELL 都显示 /bin/bash,但 ~/.bashrc 第 33 行有 exec zsh -l——交互环境实际是 zsh。往 .bashrc 末尾追加的东西在 exec 之后,永远执行不到。
判断方法:看报错格式。文件:行号: command not found: xxx 是 zsh 的格式,bash 的长得不一样。
3.9 内网下载大文件必须核对 sha256
见 §1 第四步。5.9MB 二进制被截断成 2.7MB 但仍返回 HTTP 200,同链路 366B 小文件完好。不能信 HTTP 200。
3.10 SSH 会话大多不用管,但远端中继端口会被陈旧连接卡住
正常情况不用清理:一轮下来开关 6 个 SSH 工作区,之后 ssh-session-list 只剩 1 个(对应唯一存活的工作区),零孤儿,生命周期跟着工作区走。
但有一个例外会真的出问题。 报错长这样:
Remote daemon error: Remote SSH relay...
Error: remote port forwarding failed for listen port 57279 (retry in 2s)
成因链条:
- cmux 用 SSH 远端端口转发(
-R)建中继,端口号记在~/.cmux/relay/<port>.slot一类文件里 - 陈旧的 SSH 连接(实测有存活 10~12 小时的)仍占着那个端口
- cmux 自带一段
cmux_stale_relay_listener_cleanup脚本处理这种情况——用lsof找出占端口的 sshd,核对 relay 元数据确认无主后 kill 掉 - 但
-R转发的监听套接字是由root 身份的 sshd 监控进程创建的。没有 root 权限时,lsof和ss -tlnp都看不到属主 - 脚本找不到 PID →
[ -n "$cmux_listener_pids" ] || exit 0→ 静默空转 → 无限重试
影响范围有限:终端本身是好的,坏掉的只是中继功能(内置浏览器从远端出网)。不影响敲命令。
为什么每次重启 Mac 都会撞:远端 sshd 若没配 ClientAliveInterval(默认 0 = 从不探测),你的机器一关机,远端那条连接不会被回收,实测能挂 10~12 小时不动。有 root 的话让运维配上 ClientAliveInterval 是根治办法。
免密 sudo 帮不上 cmux:那段自愈脚本直接调 lsof,源码里没有任何 sudo。配了 sudo 它也不会用,只方便你手动查(sudo ss -tlnp | grep <port>)。为这个开 root 免密不值。
不用 root 的回收办法:陈旧的 sshd: <user>@notty 进程归你自己所有,可以直接 kill。判据是有没有 cmuxd-remote 子进程——有=在用,无=陈旧。放进 ~/.zshrc:
# sshgc <ssh-host> 只列出
# sshgc <ssh-host> -f 真的杀
sshgc() {
local host="${1:?用法: sshgc <ssh-host> [-f]}" force="${2:-}"
ssh -o BatchMode=yes -o ConnectTimeout=8 "$host" "FORCE='$force' bash -s" <<'REMOTE'
self_chain=""; p=$$
while [ "$p" -gt 1 ] 2>/dev/null; do
self_chain="$self_chain $p"; p=$(ps -p "$p" -o ppid= 2>/dev/null | tr -d ' '); [ -z "$p" ] && break
done
found=0
for pid in $(ps -eo pid,user,args --no-headers | awk -v u="$USER" '$2==u && /sshd:.*@notty/ {print $1}'); do
case " $self_chain " in *" $pid "*) continue ;; esac # 别把自己这条连接杀了
kids=$(ps -eo ppid,args --no-headers | awk -v p="$pid" '$1==p && /cmuxd-remote/ {c++} END{print c+0}')
[ "$kids" != "0" ] && continue
et=$(ps -p "$pid" -o etime= 2>/dev/null | tr -d ' '); found=$((found+1))
if [ "$FORCE" = "-f" ]; then kill "$pid" 2>/dev/null && echo " 已杀 pid=$pid (运行 $et)"
else echo " 陈旧 pid=$pid (运行 $et) —— 加 -f 才真的杀"; fi
done
[ "$found" = "0" ] && echo " 没有陈旧连接"
REMOTE
}
排除当前连接祖先链那段是必须的,否则脚本会把自己所在的 ssh 连接一起杀掉。
还有一种撞法是自冲突,sshgc 治不了:同一条活着的连接,PTY 桥断开重连后 cmux 会在同一个端口重建转发,撞上自己先前那个还没释放的转发。实测表现是杀光所有陈旧连接后端口仍被占,而剩下的唯一持有者就是当前在用的那条。这种只能等——关掉该工作区或重启 cmux 即可,期间终端功能不受影响。
别做的事:不要凭 --slot 和 session id 的前缀是否匹配去判断哪个 cmuxd-remote 是孤儿然后杀掉——见 3.11。
3.11 --slot 不等于 session id,别靠前缀匹配判断孤儿
cmuxd-remote 进程的 --slot ssh-<uuid> 是 SSH 连接槽位,而 ssh-session-list 输出的 session id 是 ssh-<uuid>-<uuid> 形式的另一套标识。一个 slot 可以承载 ID 完全不相关的会话。
踩过的坑:看到某个 --persistent-server 进程的 slot(ssh-412642b9…)跟当前唯一会话 ID(ssh-5D4FF9A3…)前缀不匹配,就判定它是孤儿并 kill 掉——结果它正在服务那个活着的工作区,终端被重启,1 MB 回滚内容和正在跑的 agent 会话全丢。
--persistent-server 的 ppid=1 也不是孤儿的证据:持久化设计本来就会让它脱离父进程,这样断连才不掉线。
要判断某个远端 daemon 能不能动,可靠依据只有 ssh-session-list --workspace <ws> 逐个工作区对照,而不是进程参数。
3.12 过夜合盖后不会自动重连,且 heartbeat.age 是假指标
这条推翻了两个先前的判断,实测过程记在这。
现象:笔记本合盖过夜(约 9 小时),早上打开,三个 SSH 工作区侧栏都挂着红字:
Remote proxy to <user>@<host> unavailable:
Remote daemon transport failed: daemon transport keepalive timed out (retry in 3s)
注意这不是 3.10 那个中继端口坑——文案是 daemon transport keepalive timed out(传输层 keepalive 超时),不是 listen port NNNNN(端口冲突)。成因是合盖期间本机到远端的网络中断,cmux 的自动重连”暂停”了,醒来没有自己恢复。
别信 remote.heartbeat.age:workspace list --json 里三个会话全写 state=connected / daemon=ready / proxy=ready,但 heartbeat.age_seconds 冻在断连时刻、8~9 小时不动。一度拿这个 age 判定”连接已死”——错的。真实流量能跑通时这个字段照样不刷新,它只统计某种独立心跳、不随数据流更新。判活只能靠读屏或发探针命令看回显,不能看这个字段,也不能看 state=connected(那是缓存态)。
远端会话其实都活着:ssh-session-list --workspace <ws> 显示三个远端持久会话 attachments=1、scrollback_bytes=1048576(1 MB 满额)全在。丢的只是本机↔远端这段传输,远端 PTY 和昨晚跑的 agent 没受影响。
正确处理是 reconnect,不是 sshgc:
cmux workspace reconnect --workspace <ws> # 逐个救;救活后昨晚的东西原样都在
cmux mark-notification-read --all && cmux dismiss-notification --all-read # 清红字横幅
工作区多了逐个 reconnect 很烦,包成一个函数(§1 第三步已含):sshreconnect 只重连不健康的、跳过正常连接(不打断在跑的 agent),sshreconnect -f 强制全部。命令面板也有对应两条预设。实测重连不丢会话——远端 session id 前后一致。判活务必用 state+daemon+proxy 三个字段,别用 heartbeat。
sshgc 在这里没用——它是给”远端陈旧连接占端口”用的(3.10),而这里远端 daemon 好好的,是本机侧传输要重建。clear-notifications 也清不掉这两条红字:它们是 none 作用域的未读通知,得先 mark-notification-read --all 再 dismiss-notification --all-read。
能不能根治:不能一劳永逸,但能减轻。合盖断网这段传输必然中断(物理决定,任何 SSH 客户端都躲不掉)。三个改善入口:① 记住 reconnect 命令(零成本,推荐);② cmux ssh 加 --ssh-option ServerAliveInterval=30 --ssh-option ServerAliveCountMax=3,只对短暂抖动有效、对整夜断网无效(网络不通探测包也发不出),为整夜合盖场景改它不值;③ 远端 sshd 配 ClientAliveInterval(要 root,且治的是 3.10 的陈旧连接不是这条)。实测本机 ~/.ssh/config 无 keepalive、远端 ClientAliveInterval=0(从不探测),所以”死连接挂整晚”是必然而非偶发。
4. 实测数据
| 项目 | 实测值 |
|---|---|
| 远端 daemon 进程 | 约 2 个/连接,合计 16.1 MB RSS |
| 占远端内存比例 | 0.001%(该机 1487 GB 内存) |
远端磁盘 ~/.cmux | 6.2 MB,其中 6.0 MB 是 daemon 二进制本身(别删,删了要重推) |
| 断连重连 | workspace disconnect → reconnect 后 session ID 不变,scrollback 从 93 KB 增长到 99 KB 完整保留 |
| 过夜合盖后 | 三个会话侧栏显示 connected 但传输实际已断(见 §3.12),ssh-session-list 显示远端持久会话 attachments=1、scrollback 1 MB 满额全部存活;workspace reconnect 后恢复,昨晚跑的 agent 全在 |
| 被截断的下载 | 期望 5.9 MB,实收 2.7 MB,HTTP 200 |
远端进程不会因本机断网而死:远端跑的是远端进程,PTY 和回滚由远端 cmuxd-remote 持有。短暂断网(合盖几分钟、切网络)cmux 能自动重连接回同一会话,scrollback 不丢。
但”过夜合盖后自动重连”不可靠——实测过一次整夜合盖,醒来三个会话都卡在”connected 但传输已断”,不会自愈,要手动 cmux workspace reconnect --workspace <ws>(详见 §3.12)。远端会话本身全部存活,救回零损失。命令行里有 --persistent-lease-port 参数暗示有租约机制,TTL 未知,但这次实测过夜(约 9 小时)远端会话没被回收。
5. 已确认的实现与文档不一致
workspaceInheritWorkingDirectory: false 不会用 ghostty 的 working-directory。
schema 文档原文是”When false, new workspaces use Ghostty’s working-directory setting instead”,但实测(含重启 app 后复测)新工作区一律落在家目录 ~,无论 ghostty 那边设成什么。排查过程中排除了两个假设:
- 不是”启动时缓存”——重启后行为不变
- 不是派生配置被剥离——cmux 在
~/Library/Application Support/com.cmuxterm.app/config.ghostty生成的派生文件是空的,说明它直接读源配置渲染
而 ghostty +show-config 能正确回显 working-directory,所以问题在 cmux 侧。
结论与处理:workspaceInheritWorkingDirectory 保持默认 true(继承当前工作区目录)。设 false 会掉到家目录,比继承更难用。
要”固定从某个根目录开新工作区”,用 commands 预设显式传 --cwd,这条路是可靠的:
{
"name": "新工作区 · 99_code",
"keywords": ["new", "workspace", "99code"],
"command": "cmux workspace create --cwd ~/Documents/_work/99_code"
}
于是两种行为都有:⌘N 继承当前目录,⌘⇧P 搜预设从固定根目录开。
未验证的细节:上述测试都走 CLI
cmux workspace create。UI 的⌘N路径是否也忽略该设置没单独确认过。
6. 没配的东西
actions/ui.newWorkspace(+ 按钮菜单、自定义工作区布局):官方文档标注为 nightly 特性,稳定版可能不认。而且ui.newWorkspace.contextMenu一旦定义会替换默认菜单,如果解析失败会把 + 按钮菜单搞坏。风险不值当,等切 nightly 再说。- 整个
workspaceGroups块:2026-08-05 起不用侧栏分组(原因见 §2),所以图标/颜色/落位规则和那条byCwd.*.contextMenu(分组 + 按钮的右键菜单)都不配了。后者本来也有风险 —— schema 与上面那个 nightly 的ui.newWorkspace.contextMenu相同,一样会替换默认菜单。 - 自定义侧栏:
~/.config/cmux/sidebars/*.swift,运行时解释的 SwiftUI(beta)。cmux docs sidebars有说明。 cmux vm云端开发机:需要cmux auth login,是 cmux 自家的云环境,跟自建 SSH 机器是两套东西。- 全局 SSH 连接复用:
~/.ssh/config里给Host *加ControlMaster auto能让所有 ssh/scp/git 受益,不只 cmux 拖拽。影响面大所以没动。
← 被以下页面引用(1)
- Zsh 一键复刻配置toolbox · entity
修改历史5 次提交
- docs: refresh Kimi token flow and cmux guidancexiaocheng··
8a0f13e - docs(toolbox): auto-join group on devenv/agentenvxiaocheng··
1b9d7a0 - docs(toolbox): add sshreconnect helper for cmux remote wakexiaocheng··
d3b2a49 - docs(toolbox): correct cmux remote reconnect behaviorxiaocheng··
062a7cb - docs(toolbox): add cmux setup referencexiaocheng··
582b4b6