NeovimのSSHリモート開発 0.13の:connect ssh://の使い方
VS CodeのRemote SSHのように、手元の画面から遠隔のサーバーを触りたい。Neovimではこれまでプラグインが担ってきた役割を、0.13では本体の:connect ssh://が引き受けます。打つと、接続先に使えるNvimが無ければ自動で入れて起動し、手元の端末のUIをそこへつなぎます。
ただし、回線が切れたときの動きはtmuxと逆です。tmuxの中のNeovimはSSHが切れても残りますが、この機能で起動した遠隔のNvimは切断と同時に止まります。一方で、端末を閉じただけなら止まりません。何がどちら側で動き、切れたら何が残るのかを実機で切り分けました。
検証環境:手元はnightlyのNVIM v0.13.0-dev-2032+g254bec339c(macOS arm64、OpenSSH_10.3p1)。接続先はDockerのdebian:stable-slim(aarch64)にopenssh-serverとcurlを入れ、手元の127.0.0.1へ公開したものです。操作はtmuxの中で行い、切断はsshのプロセスを落とす、docker stop、docker network disconnectの3通りで作りました。物理的な回線断と遅い回線では試していません。安定版の0.12.6では接続の確認が出ないので、0.13で入る機能です。正式版までに変わる可能性があります。
目次(9項目)
:connect ssh://で何ができるか
Neovimの中で次の形で打ちます。ヘルプの例です。~/.ssh/configのHost名と、root@127.0.0.1:22922の形でつながることを確かめました。
:connect ssh://myhost
:connect ssh://user@192.168.1.100
:connect ssh://root@server:2222
つないでいる間のプロセスを両側で見ると、編集の本体は遠隔にあり、手元に残るのは画面を描く役とトンネルの持ち主だと分かります。長いオプションは省きました。
# 手元
nvim -n
nvim --embed -n
ssh: <キャッシュ>/nvim/ssh/mux_<40桁> [mux]
ssh -T -o BatchMode=yes -o ControlMaster=auto ... -o ControlPersist=10m ... -o ServerAliveInterval=15 -L <手元のソケット>:/tmp/nvim-remote.215232113B59743E/nvim.sock -- vt sh -s -- ...
# 遠隔
nvim --clean -n --headless +lua ... +qa!
nvim --headless --listen /tmp/nvim-remote.215232113B59743E/nvim.sock
SSHまわりの似た話とは、次のように役割が分かれます。
- VPSにSSHで入って中のVimを使う構成:画面ごと遠隔にあります
- netrwでリモートのファイルを編集する方法:ファイルだけをSCPで運びます
- Neovim 0.13の:detach!:手元のサーバーを残してつなぎ直します
- Neovim 0.13の新機能と、意味が変わるキー:0.13全体の変更です
使い始めるのに要るもの
条件はヘルプのremote-sshの項に書いてあります。今回動かしたのはmacOSからDebian(aarch64)への組み合わせだけで、それ以外はヘルプの記述です。
| 場所 | 必要なもの |
|---|---|
| 手元 | OpenSSH 7.6以降。Win32-OpenSSHは多重化が無いので対象外 |
| 認証 | 鍵認証。BatchMode=yesで動くのでパスワードを聞かれない |
| 遠隔のOS | Linux(x86_64、aarch64)かmacOS(arm64、x86_64) |
| 遠隔の道具 | curl、tar、POSIXのsh、書き込める/tmp |
ssh_configでStrictHostKeyCheckingを設定していなければ、未知のホスト鍵は確認なしで記録されます。実際のコマンドにもStrictHostKeyChecking=accept-newが付いていました。また、ヘルプによると、接続先の$PATHにあるnvimは手元と同じ版か新しければそのまま使われ、それ以外は自動で入ります。手元がプレリリース版のときは使われません。この$PATHはログインしないシェルのものなので、シェルの設定で足した場所のnvimは使われません。
つないでみる
接続先にnvimが入っていない状態で打つと、確認が出ます。vtは検証用のHost名です。
:connect ssh://vt
This will install Nvim on ssh://vt (in ~/.local/share/nvim-remote/), if missing.
Connect? [Y]es, (U)pdate Nvim, (N)o:
yで先へ進むと、手元と同じ版が~/.local/share/nvim-remote/releases/nightly-v0.13.0-linux-arm64/に展開され、画面が遠隔のものに切り替わります。入る量は64MBで、キャッシュを消して測ると約2秒、別の試行の初回は15秒ほどでした。一度入れば次からは展開が要らず、yからサーバーの起動まで0.5秒でした。
つながったかどうかは見た目では分かりません。次の1行で確かめられます。40d544728c59は接続先のホスト名です。
:lua print(vim.fn.hostname(), vim.fn.getcwd())
40d544728c59 /root
(U)pdate Nvimはnightlyを使っているときだけの選択肢です。ソースではプレリリース版にだけ追加され、それ以外はYesとNoの2択です。ヘルプによるとnightlyは一度しか入らないので、手元を新しくしても遠隔は古いままです。uを選ぶと入れ直され、ディレクトリの更新時刻が変わりました。同じものを使う他のセッションを壊すことがあるとヘルプにあります。
何が遠隔で動くのか
つないだあとのNeovimはNVIM_APPNAME=nvim-remoteで動いています。置き場所はすべて通常のNeovimと別です。
:lua print(vim.fn.stdpath("config"), vim.fn.stdpath("data"), vim.fn.stdpath("state"), vim.fn.stdpath("cache"))
/root/.config/nvim-remote /root/.local/share/nvim-remote /root/.local/state/nvim-remote /root/.cache/nvim-remote
手元と遠隔のinit.luaとplugin/に目印の変数を書いて確かめると、読まれたのは遠隔の~/.config/nvim-remote/だけでした。遠隔の~/.config/nvim/も読まれません。そちらを使うならln -s ~/.config/nvim ~/.config/nvim-remoteでつなげばinit.luaが読まれました。プラグインマネージャーを含む設定までは試していません。
:lua print(vim.g.lp, vim.g.local_marker, vim.g.rp, vim.g.remote_marker)
nil nil REMOTE-PLUGIN REMOTE-INIT
ファイル以外の動きは次の表のとおりです。
| 操作 | 結果 |
|---|---|
:terminal | 遠隔のbashが開く。$NVIM_APPNAMEはnvim-remote |
system() | 遠隔で実行される。pbcopyは見つからない |
| LSP | 遠隔のプロセスとして起動する |
:Explore | 遠隔の/rootの一覧が出る |
:e /Users/... | 手元のファイルは開けず、遠隔の新規バッファになる |
LSPは、ホスト名を記録するだけの小さなサーバーをvim.lsp.start()で動かして確かめました。遠隔のNvimの子プロセスになったので、言語サーバーは接続先に入れます。
クリップボードは、今回の環境では届きませんでした。"+yyを押すとclipboard: No provider. Try ":checkhealth" or ":h clipboard".と出ます。ヘルプによると対応端末ならOSC 52が自動で検出されますが、tmuxの中では妨げられることがあり、今回はg:termfeaturesが空でした。~/.config/nvim-remote/init.luaにvim.g.clipboard = 'osc52'と書くと、コピーした行が手元のtmuxのバッファまで届きました(set-clipboard on)。tmuxの外の端末とOSのクリップボードまでは試していません。
接続が切れたら何が残るか
遠隔でファイルを編集して保存せずに、状況ごとに何が残るかを確かめました。ヘルプには、接続が切れると遠隔のNvimが止まると書かれています。
| 起きたこと | 手元の画面 | 遠隔のNvim | 未保存の編集 |
|---|---|---|---|
| sshプロセスが死ぬ | すぐシェルに戻る | 止まる | スワップに残る |
:detach!のあとで同じ | すぐシェルに戻る | 止まる | スワップに残る |
| サーバーの停止 | 数秒以内にシェルに戻る | 止まる | スワップに残る |
| 通信だけ途絶 | 1分前後でシェルに戻る | 途絶中は残り、復旧後に止まる | スワップに残る |
| 端末を閉じる | 消える | 動き続ける | バッファに残る |
| 遠隔のtmuxの中で切断 | シェルに戻る | 動き続ける | バッファに残る |
間違えやすいのは2段目です。:detach!で手元のサーバーは守れても、ここでは守れません。:detach!を打つとNvim will continue running if the UI disconnectsと表示されるのに、SSHが切れると止まりました。ソースを読むと、上の一覧の--cleanのプロセスがSSHの切断を標準入力の終わりで検知し、Nvim本体を止めるように書かれています。
サーバーを止めただけなら待たされません。docker stopでは、シェルに戻るまで0〜4秒でした。sshdが止まって接続が閉じるからだと読んでいますが、原因までは確かめていません。
待つのは、通信だけが途絶えて応答が消えたときです。切断をServerAliveInterval=15の応答なしで検知するためで、ソースのコメントには45秒以内とあります。Docker経由のdocker network disconnectで測ると55秒から87秒まで幅があり、差の理由は追っていません。電源が落ちた場合は試していません。
スワップから戻す
未保存の編集は、遠隔の~/.local/state/nvim-remote/swap/に残っていました。つなぎ直して同じファイルを開くと、見慣れたE325が出ます。
E325: ATTENTION
Found a swap file by the name "~/.local/state/nvim-remote/swap/%root%hello.py.swp"
owned by: root dated: Fri Nov 06 11:11:54 2026
file name: ~root/hello.py
modified: YES
user name: root host name: 40d544728c59
process ID: 914
rで戻すと、書きかけの# unsavedが復元されました。中身まで開いたのはsshプロセスを落とした場合だけで、他の切断ではファイルが残ることを見ました。画面の読み方はE325の読み方の記事に書きました。
端末を閉じた場合と手元のサーバー
tmux kill-sessionで端末ごと消しても、手元のサーバー、ssh、遠隔のNvimは動き続けました。130秒後にトンネルのソケットへつなぎ直すと、編集中のバッファがそのまま戻りました。
nvim --remote-ui --server $TMPDIR/nvim.$USER/<ランダムな名前>/0_remote_nvim.sock
:detachを打った場合もプロセスは残りました。つなぎ直して:qa!で終えると両側のNvimが消えます。つなぎ直さなければ、確かめた130秒の間は遠隔に残り続けました。
手元で編集中のバッファがある場合は、ヘルプの記述どおり手元のサーバーが残りました。local.txtを書きかけのまま:connect ssh://vtを打っても警告は出ず、切断後もシェルに戻るだけで何も表示されません。lsofで調べたソケットへ--remote-ui --serverでつなぐと、未保存のまま戻りました。編集中のバッファが無ければ、切断でも:qでも手元のサーバーは残りません。
tmuxとの使い分け
遠隔のtmuxの中でnvimを起動してからsshを落とすと、Nvimは動き続け、同じコマンドでつなぎ直すと未保存の行が画面に残っていました(Debianのaptで入れたNVIM v0.10.4とtmux 3.5a)。
ssh -t vt tmux new -A -s work nvim hello.py
切れやすい回線ならこちらが向きます。接続先にnvimを用意したくない環境なら:connect ssh://が楽です。
失敗するとき
接続の失敗はどれも最後がE6101: SSH connection failedになるので、見るのはE5108の続きです。鍵が無いときも、パスフレーズ付きの鍵をエージェントに入れていないときも、聞き返されずに同じ形で失敗します。パスフレーズ付きの鍵は、ssh-addでエージェントに入れるとつながりました。長いパスとスタックトレースは省略しています。
E5108: Lua: .../runtime/lua/vim/net/_ssh.lua:424: Failed to detect remote system info: root@127.0.0.1: Permission denied (publickey,password).
E6101: SSH connection failed
接続先にcurlが無いと、エラーの後半はgzipとtarの失敗になります。原因はその前の行にあります。
Installing /root/.local/share/nvim-remote/releases/nightly-v0.13.0-linux-arm64...
sh: 41: curl: not found
gzip: stdin: unexpected end of file
tar: Child returned status 1
tar: Error is not recoverable: exiting now
キャッシュのパスが長いと、つなぐ前に失敗します。多重化のソケットを<キャッシュ>/nvim/ssh/mux_<40桁>に作るためです。macOSでXDG_CACHE_HOMEの長さを変えて試すと、文言は長さで2通りに分かれました。
| キャッシュのパス | 結果 |
|---|---|
| 32バイトまで | つながる |
| 33〜49バイト | unix_listener: path "...mux_<40桁>.<16文字>" too long for Unix domain socket |
| 50バイト以上 | ControlPath too long ('...' >= 104 bytes) |
/nvim/ssh/mux_と40桁で54バイト使い、sshが一時ファイル用にさらに.と16文字の17バイトを足すので、キャッシュのパスに使えるのは33バイト未満です。既定の/Users/<名前>/.cacheは名前を除いて14バイトなので、名前が19文字以上だと届きます。深いディレクトリをHOMEにしたときにも出ます。XDG_CACHE_HOMEを32バイト以内にするとつながりました。
ヘルプには:connect!もありますが、:connect! ssh://vtはこの版ではE477: No ! allowedでした。遠隔での:restartは動き、PIDが変わっても接続は保たれ、開いていたhello.pyも戻りました。
VS CodeのRemote SSHやプラグインと比べて足りないもの
サーバーを接続先へ自動で入れる形はVS Codeと同じです。差が出るのは周辺の手間です。VS Code側は公式ドキュメントの記述で、動かして比べてはいません。
| 項目 | :connect ssh:// | VS Code Remote SSH |
|---|---|---|
| パスワード認証 | 不可 | 可(鍵を推奨) |
| 拡張の配布 | 自分で遠隔に置く | remote.SSH.defaultExtensionsで自動 |
| ポート転送 | ヘルプに記述なし | Forward a Portで追加 |
同じ目的のプラグインでは、remote-nvim.nvimのREADMEにパスワード接続と、手元の設定を遠隔へコピーする機能が挙げられています。手元の設定を持っていきたいときや、パスワードでしか入れないときは、そちらが条件に合います。remote-ssh.nvimとdistant.nvimも同じ方向のプラグインですが、どれも試していません。
検証の途中、片付けのつもりでtmux kill-sessionを打ち、次にpsを見たらnvim --embedとsshがそのまま動いていて、遠隔のNvimも開いたままでした。tmuxに慣れていると、残ってほしい場面と逆に感じます。まだ検証環境で触っただけで、毎日の開発には使っていません。
まとめ
:connect ssh://hostは接続先に使えるNvimが無ければ自動で入れ、手元のUIをつなぎます。0.13で入る機能で、安定版の0.12.6では使えません- 設定は遠隔の
~/.config/nvim-remote/で、手元のinit.luaは持ち込まれません。LSPも遠隔で動きます - 回線が切れると遠隔のNvimは止まり、
:detach!でも防げません。未保存の編集はスワップから戻します - 切れても続けたい作業は遠隔のtmuxの中で動かします
- 認証は鍵のみです。パスフレーズ付きの鍵は先に
ssh-addします