我经常从VS Code Remote的集成终端进入一个长期运行的tmux session。集成终端里用code,进入tmux以后却要改用我自己写的code_。偶尔会按错,不够优雅,正好现在AI挺方便的,我决定尝试把它们统一成code。
我原来的办法
VS Code Remote会把CLI放在~/.vscode-server/cli下面,也会在/run/user/$UID中创建形如vscode-ipc-*的Unix socket。code通过环境变量VSCODE_IPC_HOOK_CLI知道应该把请求发给哪个VS Code窗口。Remote CLI源码把这个变量读作cliPipe,随后通过它向远端窗口发送请求。
我以前从别处抄了一个名为code_的脚本(唉还是大家都用stack overflow的年代)。它先找到最近更新的Remote CLI,再按照修改时间枚举IPC socket,逐个尝试:
#!/usr/bin/env bash
max_retry=10
for i in $(seq 1 "$max_retry"); do
script=$(fd -t f '^code$' "$HOME/.vscode-server/cli" -X ls -t | head -n 1)
if [[ -z $script ]]; then
echo "VS Code remote script not found" >&2
exit 1
fi
socket=$(ls -t /run/user/"$UID"/vscode-ipc-* 2>/dev/null | head -n "$i")
if [[ -z $socket ]]; then
echo "VS Code IPC socket not found" >&2
exit 1
fi
VSCODE_IPC_HOOK_CLI=$socket "$script" "$@" && exit 0
done
echo "Failed to find valid VS Code window" >&2
exit 1
这个思路假设最新的socket最可能属于我当前使用的窗口。如果失败,脚本继续尝试更早的socket。
这里还藏着一个直接的实现错误:head -n "$i"在第二次循环会返回两行,后面依次返回更多行(哈哈这个是GPT找出来的我都不知道有这回事,反正之前一直work)。它没有选出第i个socket。即使把它改成真正逐项选择,这套办法仍然解决不了核心问题。
统一名字不能只写一个alias
最直接(但丑陋)的做法是给code_加一个名为code的alias。这样只藏住了第二个名字,底下仍然保留着扫描CLI和socket的脚本,这当然不是我要的优雅实现。
我想让同一个code在集成终端和tmux中采用同一套行为,要做到这一点,我得先弄清楚Remote扩展提供的code怎样定位CLI和VS Code窗口。原生不用tmux的情况下VS Code在创建集成终端时已经通过环境变量给出了这两项信息。
VSCODE_IPC_HOOK_CLI属于启动当前shell的VS Code窗口。两个workspace同时连接同一台远端机器时,它们会各自持有一个socket,它的修改时间只说明文件最近发生过什么(一般来讲就是在vscode工作区做了什么操作),不是100%正确的当前工作区的指标,很可能我在B工作区做了什么操作之后立刻跑到A工作区的tmux里面敲code_命令,这种偶尔的情况就会出问题。总之,这个方法没有保存“这个tmux session从哪个VS Code窗口进入”这条关系。
tmux本来就能保存这条关系
想了半天是不是有什么特殊Hook,GPT调查一下才发现tmux直接有这个功能。
tmux的update-environment选项会在创建或attach session时,从客户端复制指定的环境变量。只要我从目标VS Code窗口的集成终端执行tmux attach,tmux就能把这个窗口的socket保存到对应session中。VS Code Remote的上游issue #2763也记录了这种配置方式(2020年的老东西,看来我当年查的时候没查到这个,只查到了猴版code_),以及现有pane需要主动读取session environment这一点。
我在~/.tmux.conf中加入:
set -ga update-environment VSCODE_IPC_HOOK_CLI
set -ga update-environment VSCODE_GIT_ASKPASS_NODE
第二个变量VSCODE_GIT_ASKPASS_NODE我拿来解决CLI路径问题。它指向当前VS Code Server安装中的Node可执行文件。使用标准Remote Server目录结构,可以从它得到同一套安装中的CLI:
code_cli="${VSCODE_GIT_ASKPASS_NODE%/*}/bin/remote-cli/code"
socket负责定位窗口,askpass Node路径负责定位与当前Server安装配套的code。两者都来自创建集成终端的VS Code进程,不需要扫描目录,也不需要按照时间猜测。
加载tmux配置:
tmux source-file ~/.tmux.conf
配置只会在下一次创建或attach session时从客户端取得变量。因此我先离开tmux,再从目标VS Code集成终端重新attach:
tmux attach -t my-session
让已有shell取得更新后的值
tmux会更新session environment,但已经运行的shell不会自动修改自己的environment。hmmm就是说我attach上这个tmux之后里面的zsh不会自动从tmux重读这个值,所以我得让shell在执行命令前从tmux读取最新值。
我的zsh配置使用preexec hook:
autoload -Uz add-zsh-hook
_sync_tmux_vscode_env() {
[[ -n $TMUX ]] || return
local name entry
for name in VSCODE_IPC_HOOK_CLI VSCODE_GIT_ASKPASS_NODE; do
entry=$(tmux show-environment "$name" 2>/dev/null) || {
unset "$name"
continue
}
case $entry in
"$name="*) export "$entry" ;;
-"$name") unset "$name" ;;
esac
done
}
add-zsh-hook preexec _sync_tmux_vscode_env
bash可以通过PROMPT_COMMAND做同样的事:
_sync_tmux_vscode_env() {
[[ -n $TMUX ]] || return
local name entry
for name in VSCODE_IPC_HOOK_CLI VSCODE_GIT_ASKPASS_NODE; do
entry=$(tmux show-environment "$name" 2>/dev/null) || {
unset "$name"
continue
}
case $entry in
"$name="*) export "$entry" ;;
-"$name") unset "$name" ;;
esac
done
}
PROMPT_COMMAND="_sync_tmux_vscode_env${PROMPT_COMMAND:+;$PROMPT_COMMAND}"
最后,我直接定义code成如下function,然后就可以愉快删掉code_啦:
code() {
local code_cli
code_cli="${VSCODE_GIT_ASKPASS_NODE%/*}/bin/remote-cli/code"
if [[ -z $VSCODE_GIT_ASKPASS_NODE || ! -x $code_cli ]]; then
echo "VS Code Remote CLI not found" >&2
return 127
fi
"$code_cli" "$@"
}
zsh和bash都能使用这段函数。每次执行code前,shell先取得当前tmux session保存的变量,然后调用对应Server安装中的CLI。CLI再通过VSCODE_IPC_HOOK_CLI联系正确的窗口。
验证
重新attach后,我先检查tmux session中的值:
tmux show-environment VSCODE_IPC_HOOK_CLI
tmux show-environment VSCODE_GIT_ASKPASS_NODE
再检查socket和CLI:
test -S "$VSCODE_IPC_HOOK_CLI" && echo "IPC socket is live"
code_cli="${VSCODE_GIT_ASKPASS_NODE%/*}/bin/remote-cli/code"
test -x "$code_cli" && printf '%s\n' "$code_cli"
最后执行:
source ~/.zshrc
code .
目录应该在刚才用于attach tmux的VS Code窗口中打开。换到另一个workspace时,我从那个窗口重新attach对应session,tmux就会记录那个窗口提供的变量。
结论
我原来的脚本同时做了两次时间猜测:从~/.vscode-server/cli里选最近的CLI,再从/run/user/$UID里选最近的socket。它在只有一个窗口时通常能工作,多窗口和频繁切换workspace以后就失去了明确依据。
现在,VS Code在集成终端中提供窗口和Server安装信息,tmux在attach时保存它们,shell在运行命令前读取它们。code只负责调用CLI。这个流程保留了VS Code窗口、tmux session和workspace之间原有的对应关系。
声明:人类活动,但由Codex记录内容,最终人类进行些微修订后发布。
Last modified on 2026-07-20