0%

在 tmux 里让 Claude Code 连上 VSCode:一次源码级排查

我日常的开发环境是 VSCode Remote SSH 连到一台 Linux 开发机,然后在 VSCode 的集成终端里开 tmux。Claude Code 在 VSCode 终端里直接运行时,能正常连接 IDE——可以读到我选中的代码、跳转文件。但一旦进入 tmux,状态栏就显示「Visual Studio Code disconnected」。

这篇文章记录了从现象到根因的完整排查过程。涉及 Claude Code 的 IDE 发现机制源码、tmux 环境变量继承的坑,以及最终的修复方案。

表面现象和第一个错误方向

最初的怀疑是环境变量 TERM_PROGRAM。tmux 启动子 shell 时会把这个变量覆写为 tmux,而 Claude Code 可能依赖它来判断自己是否运行在 VSCode 终端里。

所以我在 .bashrc 里加了逻辑:检测到 tmux session 中有 VSCODE_IPC_HOOK_CLI 时,就强制 export TERM_PROGRAM=vscode

试了一圈,没用。TERM_PROGRAM 改对了,IPC socket 也活着,但 Claude Code 仍然报 disconnected。

第二个错误方向:$TMUX 变量

接下来怀疑 Claude Code 检测到 $TMUX 环境变量就直接拒绝连接。试了 TMUX= claude 启动——清掉 $TMUX 后再跑,还是不行。

找到源码,看真正的逻辑

Claude Code 曾经短暂开源过。从源码的 utils/ide.ts 里,我找到了 IDE 检测的完整链路:

1
2
3
4
5
6
7
export async function detectIDEs(includeInvalid: boolean): Promise<DetectedIDEInfo[]> {
const ssePort = process.env.CLAUDE_CODE_SSE_PORT
const envPort = ssePort ? parseInt(ssePort) : null
const cwd = getOriginalCwd().normalize('NFC')
const lockfiles = await getSortedIdeLockfiles()
// ...
}

整个发现机制是这样的:

  1. VSCode 的 Claude Code 扩展启动后,在 ~/.claude/ide/ 下创建一个 {port}.lock 文件,内容是 JSON,包含 workspaceFolderspidtransport 等信息。
  2. CLI 端的 Claude Code 启动时,扫描所有 lockfile,尝试匹配。
  3. 匹配的优先级:先看环境变量 CLAUDE_CODE_SSE_PORT 是否指向某个 lockfile 的端口;如果没有,就用 cwd 去匹配 lockfile 里的 workspaceFolders

第一个真正的问题:过期的 CLAUDE_CODE_SSE_PORT

VSCode 在启动集成终端时,会注入一个 CLAUDE_CODE_SSE_PORT 环境变量,指向当前窗口对应的扩展端口。问题是:进入 tmux 后,这个变量被「冻结」在了创建 session 时的值。如果中间 VSCode 重启过、端口变了,tmux 里的变量就指向一个不存在的 lockfile。

验证一下:

1
2
3
4
5
$ echo $CLAUDE_CODE_SSE_PORT
30322

$ ls ~/.claude/ide/30322.lock
ls: cannot access '~/.claude/ide/30322.lock': No such file or directory

端口 30322 已经不存在了,但环境变量还指着它。Claude Code 拿这个端口去匹配,找不到对应的 lockfile,自然连不上。

第二个真正的问题:祖先进程检查

清掉过期的 CLAUDE_CODE_SSE_PORT 之后,按理说 Claude Code 应该回退到 cwd 匹配模式。但它仍然找不到 IDE。继续看源码:

1
2
3
4
5
6
7
8
9
10
11
12
13
const needsAncestryCheck = getPlatform() !== 'wsl' && isSupportedTerminal()

if (needsAncestryCheck) {
const portMatchesEnv = envPort !== null && lockfileInfo.port === envPort
if (!portMatchesEnv) {
if (process.ppid !== lockfileInfo.pid) {
const ancestors = await getAncestors()
if (!ancestors.has(lockfileInfo.pid)) {
continue // 跳过这个 lockfile
}
}
}
}

关键在 isSupportedTerminal()。这个函数检查 env.terminal 是否是已知的 IDE 类型(vscode、cursor、windsurf 等)。因为我前面把 TERM_PROGRAM 强制设成了 vscode,所以 isSupportedTerminal() 返回 true,触发了祖先进程检查。

祖先进程检查做的事情是:确认 lockfile 里记录的 IDE pid 是当前 Claude Code 进程的某个祖先。在 VSCode 集成终端里直接运行时,进程树是 VSCode(node) → shell → claude,检查能通过。但在 tmux 里:

1
2
3
tmux: server (pid 1746497)
└─ bash (pid 1934140)
└─ claude (pid 1934358)

IDE 的 pid(1845751)完全不在这条祖先链上。检查失败,lockfile 被跳过。

根因总结

两个问题叠加:

  1. 过期的 CLAUDE_CODE_SSE_PORT:tmux 继承了旧的端口号,lockfile 已不存在。
  2. 错误地伪装 TERM_PROGRAM=vscode:触发了本不该触发的祖先进程检查,而这个检查在 tmux 里必然失败。

修复

最终的 .bashrc 配置:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# tmux + VSCode: auto-sync IPC socket so Claude Code can connect to VSCode
if [ -n "$TMUX" ]; then
_update_vscode_ipc() {
local v
v=$(tmux show-environment VSCODE_IPC_HOOK_CLI 2>/dev/null)
if [[ "$v" == VSCODE_IPC_HOOK_CLI=* ]]; then
export "$v"
fi
# Clear stale SSE port if its lockfile no longer exists
if [[ -n "$CLAUDE_CODE_SSE_PORT" && ! -f "$HOME/.claude/ide/${CLAUDE_CODE_SSE_PORT}.lock" ]]; then
unset CLAUDE_CODE_SSE_PORT
fi
# Do NOT set TERM_PROGRAM=vscode in tmux — it triggers ancestor PID check which fails
}
PROMPT_COMMAND="_update_vscode_ipc;${PROMPT_COMMAND:-}"
fi

核心思路:

  • 不伪装 TERM_PROGRAM。让 isSupportedTerminal() 返回 false,跳过祖先进程检查。
  • 清理过期的 CLAUDE_CODE_SSE_PORT。当端口对应的 lockfile 不存在时 unset 它,让 Claude Code 回退到纯 cwd 匹配模式。
  • 保持 VSCODE_IPC_HOOK_CLI 的同步。这个变量用于其他 VSCode CLI 功能(比如 code 命令),继续从 tmux session 环境里刷新。

修复后,在 tmux 里启动 Claude Code,用 /ide 命令手动触发一次连接,显示「Connected to Visual Studio Code.」——之后就能正常读取 VSCode 中打开的文件和选中内容了。

小结

这个问题的排查过程其实挺典型的:表面上看是「环境变量没传进去」,实际上是多个机制交织——lockfile 发现、端口匹配、进程树校验——任何一环出问题都会导致连接失败,而且错误提示只有一个笼统的「disconnected」。

如果没有源码,我大概只能靠猜和试。有了源码之后,三个检查条件一目了然:端口匹配、cwd 匹配、祖先进程匹配。然后反推出「在 tmux 里,不应该伪装成 IDE 的内置终端,而应该走纯 workspace 路径匹配」这个结论。

对于同样在 tmux 里使用 Claude Code 的人,记住两点就够了:别设 TERM_PROGRAM=vscode,以及确保不要残留一个过期的 CLAUDE_CODE_SSE_PORT。剩下的交给 lockfile 的 cwd 匹配机制就好。

俗话说,投资效率是最好的投资。 如果您感觉我的文章质量不错,读后收获很大,预计能为您提高 10% 的工作效率,不妨小额捐助我一下,让我有动力继续写出更多好文章。