<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom"><channel><title>Remote-Dev on 李寒的小窝</title><link>http://lihan3238.github.io/tags/remote-dev/</link><description>Recent content in Remote-Dev on 李寒的小窝</description><generator>Hugo -- gohugo.io</generator><language>zh</language><lastBuildDate>Thu, 04 Jun 2026 20:33:33 +0800</lastBuildDate><atom:link href="http://lihan3238.github.io/tags/remote-dev/index.xml" rel="self" type="application/rss+xml"/><item><title>Codex 从 Mac 远连 Windows——绕过 pwsh，走 WSL 的 sshd</title><link>http://lihan3238.github.io/p/codex-remote-to-windows-via-wsl-ssh/</link><pubDate>Thu, 21 May 2026 00:00:00 +0800</pubDate><guid>http://lihan3238.github.io/p/codex-remote-to-windows-via-wsl-ssh/</guid><description>&lt;img src="http://lihan3238.github.io/p/codex-remote-to-windows-via-wsl-ssh/imgs/avatar.png" alt="Featured image of post Codex 从 Mac 远连 Windows——绕过 pwsh，走 WSL 的 sshd" /&gt;
 &lt;blockquote&gt;
 &lt;p&gt;本文是一次真实配置的完整走查记录（一次性、面向人阅读）。这套环境配好一次就长期
复用，我大概率不会再从头踩坑，所以收成博文而不是知识卡片。&lt;/p&gt;

 &lt;/blockquote&gt;
&lt;h2 id="背景与问题"&gt;背景与问题
&lt;/h2&gt;&lt;p&gt;想用 Codex 的 remote-connect 功能从 Mac 驱动一台 Windows 机器。连上之后，Codex
在远端跑 bootstrap 一上来就失败：报 pwsh 看不懂的 token、&lt;code&gt;&amp;amp;&amp;amp;&lt;/code&gt;、&lt;code&gt;$(...)&lt;/code&gt; 之类的
错误，或者客户端报远端 shell 没返回预期的握手。&lt;/p&gt;
&lt;p&gt;根因是 &lt;strong&gt;shell 不匹配&lt;/strong&gt;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Codex 的远端 bootstrap 假定远端是 POSIX shell（sh / bash）。&lt;/li&gt;
&lt;li&gt;Windows 自带的 OpenSSH server 默认把 pwsh（或 cmd）当登录 shell，所以那些
一行式 bootstrap 命令在 Codex 启动之前就先炸了。&lt;/li&gt;
&lt;li&gt;同一台 Windows 上的 WSL Ubuntu 里有真正的 bash。&lt;strong&gt;解法就是把 SSH 会话落进
WSL，而不是落在 Windows 本体。&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;适用场景：用 Mac 通过 Codex remote-connect（或其他假定 sh 的远程开发工具，比如
JetBrains Gateway、bootstrap 脚本只认 sh 的 VS Code Remote-SSH）去驱动一台装了
WSL 的 Windows 机器，且连接在 bootstrap 阶段挂在 shell 不兼容 / pwsh /
command-not-found 上。&lt;/p&gt;
&lt;p&gt;不适用：远端本身就是原生 Linux（没有 shell 不匹配）、远程工具明确支持 Windows
侧 pwsh、或者那台 Windows 上压根没装 WSL 发行版。&lt;/p&gt;
&lt;h2 id="四步配置"&gt;四步配置
&lt;/h2&gt;&lt;p&gt;第 1–3 步每台 Windows 主机一次性；第 4 步每台 Mac 一次性。&lt;/p&gt;
&lt;h3 id="1-在-wsl-ubuntu-里装-sshd"&gt;1. 在 WSL Ubuntu 里装 sshd
&lt;/h3&gt;&lt;p&gt;在 Windows 上打开 WSL Ubuntu：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1
&lt;/span&gt;&lt;span class="lnt"&gt;2
&lt;/span&gt;&lt;span class="lnt"&gt;3
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;sudo apt update
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;sudo apt install -y openssh-server
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;sudo service ssh start
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;确认：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1
&lt;/span&gt;&lt;span class="lnt"&gt;2
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;which sshd
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;sudo service ssh status
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;h3 id="2-把-mac-公钥写进-wsl-的-authorized_keys"&gt;2. 把 Mac 公钥写进 WSL 的 authorized_keys
&lt;/h3&gt;&lt;p&gt;在 Mac 上：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;cat ~/.ssh/id_ed25519.pub
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;复制整行。进到 WSL 里：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1
&lt;/span&gt;&lt;span class="lnt"&gt;2
&lt;/span&gt;&lt;span class="lnt"&gt;3
&lt;/span&gt;&lt;span class="lnt"&gt;4
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;mkdir -p ~/.ssh
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;chmod &lt;span class="m"&gt;700&lt;/span&gt; ~/.ssh
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;nano ~/.ssh/authorized_keys &lt;span class="c1"&gt;# 粘贴 Mac 公钥，保存&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;chmod &lt;span class="m"&gt;600&lt;/span&gt; ~/.ssh/authorized_keys
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;h3 id="3-把-windows-的-2222-转发到-wsl-的-22"&gt;3. 把 Windows 的 :2222 转发到 WSL 的 :22
&lt;/h3&gt;&lt;p&gt;在 Windows 上以&lt;strong&gt;管理员&lt;/strong&gt; PowerShell 执行：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt; 1
&lt;/span&gt;&lt;span class="lnt"&gt; 2
&lt;/span&gt;&lt;span class="lnt"&gt; 3
&lt;/span&gt;&lt;span class="lnt"&gt; 4
&lt;/span&gt;&lt;span class="lnt"&gt; 5
&lt;/span&gt;&lt;span class="lnt"&gt; 6
&lt;/span&gt;&lt;span class="lnt"&gt; 7
&lt;/span&gt;&lt;span class="lnt"&gt; 8
&lt;/span&gt;&lt;span class="lnt"&gt; 9
&lt;/span&gt;&lt;span class="lnt"&gt;10
&lt;/span&gt;&lt;span class="lnt"&gt;11
&lt;/span&gt;&lt;span class="lnt"&gt;12
&lt;/span&gt;&lt;span class="lnt"&gt;13
&lt;/span&gt;&lt;span class="lnt"&gt;14
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-powershell" data-lang="powershell"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nv"&gt;$wslIp&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;wsl&lt;/span&gt; &lt;span class="n"&gt;hostname&lt;/span&gt; &lt;span class="n"&gt;-I&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="py"&gt;Trim&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="py"&gt;Split&lt;/span&gt;&lt;span class="p"&gt;()[&lt;/span&gt;&lt;span class="mf"&gt;0&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;netsh&lt;/span&gt; &lt;span class="n"&gt;interface&lt;/span&gt; &lt;span class="n"&gt;portproxy&lt;/span&gt; &lt;span class="n"&gt;add&lt;/span&gt; &lt;span class="n"&gt;v4tov4&lt;/span&gt; &lt;span class="p"&gt;`&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;listenaddress&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="mf"&gt;0.0&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="py"&gt;0&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="py"&gt;0&lt;/span&gt; &lt;span class="p"&gt;`&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;listenport&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="mf"&gt;2222&lt;/span&gt; &lt;span class="p"&gt;`&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;connectaddress&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="nv"&gt;$wslIp&lt;/span&gt; &lt;span class="p"&gt;`&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;connectport&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="mf"&gt;22&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nb"&gt;New-NetFirewallRule&lt;/span&gt; &lt;span class="p"&gt;`&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;-DisplayName&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;WSL SSH 2222&amp;#34;&lt;/span&gt; &lt;span class="p"&gt;`&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;-Direction&lt;/span&gt; &lt;span class="n"&gt;Inbound&lt;/span&gt; &lt;span class="p"&gt;`&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;-Action&lt;/span&gt; &lt;span class="n"&gt;Allow&lt;/span&gt; &lt;span class="p"&gt;`&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;-Protocol&lt;/span&gt; &lt;span class="n"&gt;TCP&lt;/span&gt; &lt;span class="p"&gt;`&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;-LocalPort&lt;/span&gt; &lt;span class="mf"&gt;2222&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;h3 id="4-在-mac-的-ssh-config-里加一个-host-条目"&gt;4. 在 Mac 的 SSH config 里加一个 Host 条目
&lt;/h3&gt;&lt;p&gt;编辑 Mac 上的 &lt;code&gt;~/.ssh/config&lt;/code&gt;：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1
&lt;/span&gt;&lt;span class="lnt"&gt;2
&lt;/span&gt;&lt;span class="lnt"&gt;3
&lt;/span&gt;&lt;span class="lnt"&gt;4
&lt;/span&gt;&lt;span class="lnt"&gt;5
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-fallback" data-lang="fallback"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;Host lihan_pc_02_wsl
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; HostName &amp;lt;windows-host-lan-ip&amp;gt; # 例如 10.88.0.6
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; Port 2222
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; User &amp;lt;wsl-username&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; IdentityFile ~/.ssh/id_ed25519
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;然后在 Codex 里把 remote-connect 指向 Host 别名 &lt;code&gt;lihan_pc_02_wsl&lt;/code&gt;——Codex 看到
的是一个 POSIX 远端，bootstrap 就能干净跑通。&lt;/p&gt;
&lt;h2 id="踩坑点"&gt;踩坑点
&lt;/h2&gt;&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;WSL IP 每次重启都会漂。&lt;/strong&gt; 第 3 步把 &lt;code&gt;$wslIp&lt;/code&gt; 当成一个快照写死了。一旦
&lt;code&gt;wsl --shutdown&lt;/code&gt; 或 Windows 重启，要重跑 &lt;code&gt;netsh interface portproxy add&lt;/code&gt;
（先用 &lt;code&gt;netsh interface portproxy delete v4tov4 listenaddress=0.0.0.0 listenport=2222&lt;/code&gt; 删掉旧的）。可以把它写成一个开机自动跑的计划任务。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;别把代理塞进 git config。&lt;/strong&gt; 这台机器上别处用的 HTTP 代理
（&lt;code&gt;10.88.0.6:10808&lt;/code&gt;）跟这条 SSH 路径无关，别混在一起。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;连进 WSL，不是连进 Windows。&lt;/strong&gt; 如果 Codex 客户端直接落在 Windows 本体的
22 端口，你又回到 pwsh 失败的原点了。整套方案的意义就是用 2222 → WSL:22
这一跳，强制把会话顶进 WSL。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="怎么确认连通"&gt;怎么确认连通
&lt;/h2&gt;&lt;p&gt;在 Mac 上：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;ssh lihan_pc_02_wsl -o &lt;span class="nv"&gt;BatchMode&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;yes -o &lt;span class="nv"&gt;ConnectTimeout&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="m"&gt;5&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;uname -a&amp;#39;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;返回一个 Linux 内核串（形如 &lt;code&gt;Linux ... microsoft-standard-WSL2&lt;/code&gt;）且退出码为
0，就说明这台 Host 上的 Codex remote-connect 能用了。&lt;/p&gt;
&lt;p&gt;连不上时按顺序排查：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;WSL sshd 起没起：&lt;code&gt;sudo service ssh status&lt;/code&gt;；&lt;/li&gt;
&lt;li&gt;Windows portproxy 是否还指向当前 WSL IP：对比
&lt;code&gt;netsh interface portproxy show v4tov4&lt;/code&gt; 与 &lt;code&gt;wsl hostname -I&lt;/code&gt;
（每次 WSL 重启 IP 都会漂）；&lt;/li&gt;
&lt;li&gt;防火墙规则 &lt;code&gt;WSL SSH 2222&lt;/code&gt; 是否存在；&lt;/li&gt;
&lt;li&gt;&lt;code&gt;~/.ssh/authorized_keys&lt;/code&gt; 里有没有 Mac 公钥，且权限是 600。&lt;/li&gt;
&lt;/ol&gt;</description></item></channel></rss>