$ OSC passthrough Emacs vterm + ssh + tmux で
作業ディレクトリが反映されなかった

Emacs の vterm から ssh して、さらに tmux の中で作業する。
この構成はとても快適ですが、「Emacs 側のカレントディレクトリが全然追従しない」ことに気づきました。

最初は設定ミスだと思っていたのですが、実際には複数の要因が重なっていました。

関連記事

1. vterm のディレクトリ追跡が動かない

症状は単純でした。
Emacs → vterm → ssh → Lubuntu → bash → tmux という構成で、

  • ssh 先で cd しても
  • tmux の中でディレクトリを移動しても

Emacs 側のvterm上のモードラインにカレントディレクトリを表示させたいのですが、 default-directory が変わりません。
ローカルの vterm では問題なく動くようにできたので、「ssh か tmux のどちらかが怪しい」と感じました。

2. vterm が何を見ているのか

調べていくと、vterm はシェルの状態をシェル側から送られる制御シーケンスをそのまま信じている、という仕組みだと分かりました。
ここで出てくるのが OSC(Operating System Command)です。

OSC 51;A という形式のシーケンスを送ると、「ユーザー名・ホスト名・カレントディレクトリ」を Emacs に教えられます。

つまり、

  • ssh 先の bash が
  • プロンプト表示のたびに
  • このシーケンスを端末へ出力している

必要があります。
したがって、ローカルに設定するだけでなく、ssh 先の bash に書く必要があります。

3. bash と zsh の違いで一度つまずく

最初に見つけた設定例は zsh 用でした。

precmd というフックを使う形です。

ただし、bash には precmd がありません。
その代わりに PROMPT_COMMAND という仕組みがあります。

これは「プロンプトが表示される直前に実行されるコマンド」を指定する変数です。

vterm_prompt_end() {
  printf '\e]51;A%s@%s:%s\e\\' "$(whoami)" "$(hostname)" "$(pwd)"
}
PROMPT_COMMAND=vterm_prompt_endCode language: JavaScript (javascript)

ssh だけなら、これで反映されました。
ただし tmux に入った瞬間、また動かなくなります。

4. tmux がエスケープシーケンスを止めている

ここからが少しややこしいところです。

tmux は「端末の中に端末を作る」ツールです。
その都合上、OSC のような制御シーケンスをそのまま外に流さず、いったん受け止めます。

結果として、

  • bash は正しく出力している
  • でも Emacs まで届かない

という状態になっていました。

5. tmux / screen 対応の vterm_printf

そこで使うのが、よく知られている vterm_printf という関数です。
tmux や GNU screen(screen という別の端末多重化ツール)を検出し、適切な形式で OSC を送ります。

if declare -p PROMPT_COMMAND >/dev/null 2>&1; then
  if [[ "$(declare -p PROMPT_COMMAND 2>/dev/null)" =~ "declare -a" ]]; then
    PROMPT_COMMAND+=(vterm_prompt_end)
  else
    PROMPT_COMMAND=(${PROMPT_COMMAND:+$PROMPT_COMMAND} vterm_prompt_end)
  fi
else
  PROMPT_COMMAND=(vterm_prompt_end)
fiCode language: PHP (php)

これで「tmux を意識した出力」はできるようになりました。
しかし、まだ反映されませんでした。

6. tmux の allow-passthrough という壁

原因は tmux の設定でした。

tmux 3.4 では、外部へ制御シーケンスを流すために allow-passthrough を有効にする必要があります。
デフォルトでは off でした。

tmux show -gqv allow-passthrough

この結果が off だったため、どれだけ正しく包んでも外に出ていませんでした。
そこで ~/.tmux.conf に、

set -g allow-passthrough onCode language: JavaScript (javascript)

を書くことで解決しました。