Repository navigation
Description
中文摘要: 桌面端 2.0.2 在 Windows 上无法检测/连接 WSL(Debian)里的 opencode:即使 ~/.opencode/bin/opencode 已安装且版本与桌面端一致,界面始终显示"未安装 OpenCode";点击"安装 OpenCode"后二进制实际已装上,但 UI 报请求失败,依旧无法添加/连接 WSL 服务器。根因是桌面端把 shell 脚本作为命令行参数传给 wsl.exe(wsl -d <distro> -- sh -lc "<script>"),WSL 会在目标 sh 执行之前对参数做一次 shell 式展开($VAR 被替换、$(...) 被求值),脚本因此被破坏。改用 stdin 传脚本(或加 --exec)即可修复。详细证据见下。
English
Summary
On Windows, OpenCode Desktop 2.0.2 shows "OpenCode not installed" for a WSL2 distro even though ~/.opencode/bin/opencode (v2.0.2, matching the Desktop version) is installed and runs correctly. Clicking Install OpenCode actually installs the binary, but the request fails with a generic UnknownError: An error occurred in Effect.tryPromise, and the dialog keeps showing "OpenCode not installed" — the WSL server can never be added or connected.
Root cause: the Desktop passes shell scripts as command-line arguments to wsl.exe:
wsl -d <distro> -- sh -lc "<script>"
WSL performs a shell-style expansion pass over those arguments before the target sh receives them: $VAR tokens are expanded from the distro environment and $(...) command substitutions are executed. The scripts generated by the Desktop (RemoteCli.discoverScript, the verify step of RemoteCli.installScript) rely on runtime expansion, so they arrive already corrupted.
Environment
- OpenCode Desktop 2.0.2 (Windows, packaged)
- Windows 11 (build 10.0.26200)
- WSL 2.6.1.0 (kernel 6.6.87.2-1)
- Distro: Debian (WSL2, default,
systemd=true) - opencode inside distro:
~/.opencode/bin/opencode→opencode v2.0.2(same as Desktop)
Reproduction
- On Windows 11 with WSL2, have a Debian distro without opencode inside.
- Desktop 2.0.2 → Add WSL server → the distro probe succeeds, but it shows "OpenCode not installed".
- Click Install OpenCode: the binary is really installed (
~/.opencode/bin/opencode, verified in a terminal) but the UI reports a failed request and still shows "not installed". Re-checking/relaunching does not help.
Mechanical reproduction (Windows, Node — this is exactly how the Electron main process spawns wsl):
const { spawnSync } = require("node:child_process")
const script = `cli=""
if [ -z "$cli" ] && [ -x "$HOME/.opencode/bin/opencode" ]; then cli="$HOME/.opencode/bin/opencode"; fi
if [ -n "$cli" ]; then printf '%s\\n' "$cli"; fi
`
const r = spawnSync("wsl", ["-d", "Debian", "--", "sh", "-lc", script], { encoding: "utf8" })
console.log("stdout:", JSON.stringify(r.stdout)) // => "" ; expected: /home/<user>/.opencode/bin/opencodeEvidence
Dumping /proc/$$/cmdline from inside the executed script shows what sh actually received.
Original script sent by the app:
cli=""
if [ -z "$cli" ] && [ -x "$HOME/.opencode/bin/opencode" ]; then cli="$HOME/.opencode/bin/opencode"; fi
if [ -n "$cli" ]; then printf '%s\n' "$cli"; fiWhat sh actually received (variables were already expanded):
cli=""
if [ -z "" ] && [ -x "/home/<user>/.opencode/bin/opencode" ]; then cli="/home/<user>/.opencode/bin/opencode"; fi
if [ -n "" ]; then printf '%s\n' ""; fiThe final condition is always false, so resolveWslCli() always returns empty and the Desktop always reports "not installed".
The install verification step suffers the same fate:
original : test "$("$HOME/.opencode/bin/opencode" --version | awk '{print $NF}' | sed 's/^v//')" = '2.0.2'
received : test "" = '2.0.2'
That is why the install reports failure after the binary has been installed successfully.
No Windows-side shell is involved (child_process.spawn passes arguments verbatim) — the expansion is performed by WSL itself.
Control experiments
Same Node spawn, same distro, same script:
| Invocation | Result |
|---|---|
wsl -d Debian -- sh -lc <script> (current code) |
empty output ❌ |
wsl -d Debian --exec sh -lc <script> |
correct path ✅ |
wsl -d Debian -e sh -lc <script> |
correct path ✅ |
script over stdin: wsl -d Debian -- bash -se + write to stdin |
correct ✅ |
wsl --help documents --exec, -e <CommandLine> as "Execute the specified command without using the default Linux shell"; without it, the command line runs through the default Linux shell, which is what causes the extra expansion.
Notably, spawnWslSidecar in packages/desktop/src/main/wsl/sidecar.ts already passes its launch script via stdin (bash -se), so the sidecar itself is unaffected: once detection/verification is bypassed, opencode serve starts and /api/health over 127.0.0.1 returns 200. The failure is entirely in the detect/install/verify steps.
Why this is different from existing issues
- Desktop 1.18.4 + WSL2 Ubuntu: shows 'OpenCode not installed' after install; version check fails #38309 (Desktop 1.18.x) was about only accepting the fixed
$HOME/.opencode/bin/opencodepath; PRs fix(desktop): resolve WSL OpenCode from PATH #44514 / fix(desktop): use PATH for WSL probe #44512 propose PATH-based resolution. - This report is a different root cause in the current v2 code: the fixed path exists and is correct, but the script that checks it is corrupted by
wsl.exeargument handling. PATH-based resolution alone will not fix it.
Suggested fix
In packages/desktop/src/main/wsl/runtime.ts:
- Preferred: stop passing scripts as
wsl.exearguments; write them to stdin (same pattern assidecar.ts) forrunWslSh/resolveWslCli/readWslCliVersion/installWslCli. - Minimal alternative: add
--exec/-einwslArgs()so arguments bypass the default Linux shell. - Add a regression test that exercises the real argv path on Windows; the current
servers.test.tsmocks the probes and cannot catch this.
Workaround (user side)
Expose a distro environment variable named cli to WSL so the pre-expansion yields the right answer:
- Windows user env:
cli=/home/<user>/.opencode/bin/opencodeandWSLENV=cli, then fully restart the Desktop app.
Verified locally: detection passes, the server is added, sidecar starts, health check returns 200. (Install/update may still show the generic error even though the binary gets installed.)
中文
现象
Windows 上的 OpenCode Desktop 2.0.2 中,新增 WSL 服务器对话框能识别 Debian,但始终显示 "未安装 OpenCode";实际在 WSL 里 ~/.opencode/bin/opencode 已安装且版本为 v2.0.2,与桌面端一致。点击"安装 OpenCode"后二进制确实被装上,但界面报请求失败(UnknownError: An error occurred in Effect.tryPromise),依旧无法添加/连接服务器。
根因
桌面端把 shell 脚本作为参数传给 wsl.exe:
wsl -d <distro> -- sh -lc "<script>"
WSL 会在目标 sh 收到脚本之前,先对这些参数做一次 shell 式展开:$VAR 会从发行版环境变量取值替换,$(...) 会被直接执行。而桌面端生成的脚本(RemoteCli.discoverScript、安装脚本里的版本校验)依赖运行时展开,于是到达 sh 时已经被破坏。
证据
从脚本内部 dump /proc/$$/cmdline,可以看到 sh 实际收到的内容:
原始脚本(应用发送):
if [ -n "$cli" ]; then printf '%s\n' "$cli"; fish 实际收到:
if [ -n "" ]; then printf '%s\n' ""; fi最后一个判断永远为假 → resolveWslCli() 永远返回空 → 界面永远显示"未安装"。
安装校验脚本同理:
原始: test "$("$HOME/.opencode/bin/opencode" --version | awk '{print $NF}' | sed 's/^v//')" = '2.0.2'
收到: test "" = '2.0.2'
所以"安装成功但报失败"。
对照实验
| 调用方式 | 结果 |
|---|---|
wsl -d Debian -- sh -lc <脚本>(现状) |
空输出 ❌ |
wsl -d Debian --exec sh -lc <脚本> |
正确输出路径 ✅ |
wsl -d Debian -e sh -lc <脚本> |
正确 ✅ |
走 stdin(bash -se) |
正确 ✅ |
wsl --help 对 --exec, -e 的说明是"在不使用默认 Linux shell 的情况下执行指定的命令";不带它时命令行会经默认 shell 执行,这正是二次展开的来源。而 sidecar.ts 启动服务器时本来就是走 stdin 的,所以服务器进程本身没问题(绕过检测后,/api/health 返回 200)。
与 #38309 等的区别
#38309(1.18.x)的问题是只认固定路径 $HOME/.opencode/bin/opencode,#44514 / #44512 提议从 PATH 解析;本 issue 是另一个根因:固定路径存在且正确,但检查它的脚本被 wsl.exe 的参数处理破坏。仅做 PATH 解析不能修复本问题。
建议修复
packages/desktop/src/main/wsl/runtime.ts:
- 首选:脚本一律改走 stdin(照
sidecar.ts的写法),覆盖runWslSh/resolveWslCli/readWslCliVersion/installWslCli; - 最小改动:
wslArgs()里加--exec/-e; - 补一条真实经过
wsl.exe参数的回归测试(现有servers.test.ts全是 mock,测不到)。
用户侧临时方案
在 Windows 用户环境变量里设置 WSLENV=cli、cli=/home/<user>/.opencode/bin/opencode,完全重启桌面端。原理:WSL 的预展开用的就是发行版环境变量,提前把路径"喂"进去。实测检测通过、可添加服务器、sidecar 正常启动、健康检查 200。
All paths and usernames in this report have been anonymized. No personal data is included.
Activity
tomclassplus commentedon Sep 12, 2026
Regression bisect: introduced by
d39290fbb9(PR #47753, 2026-09-09)I traced the history of
packages/desktop/src/main/wsl/runtime.tsand found the exact change that turned this into a release regression.Culprit commit:
d39290fbb9a57c442fd8a51f2e147b50a81178d8— feat(desktop): add SSH server connections (#47753), authored 2026-09-09. That PR refactored the WSL CLI resolution/installation to reuse the new sharedRemoteClihelpers written for SSH — where arguments are not re-expanded bywsl.exe. The WSL path inherited scripts that depend on runtime shell expansion, and that is what breaks.Git confirms
d39290fbb9is an ancestor of every V2 desktop release so far: v2.0.0 (2026-09-11), v2.0.1 and v2.0.2 (2026-09-12). The 1.18.x line is unaffected (different code path).The two changes that break WSL
1) Detection —
resolveWslCli()Before #47753 the embedded script only referenced
$HOME, which WSL's pre-expansion replaces with the correct value anyway:After #47753 it becomes
RemoteCli.discoverScript(), which depends on a runtime shell variable:Under WSL's argument pre-expansion
$cliis replaced with the empty string beforeshruns, so the final condition is always false andresolveWslCli()always returns empty → "OpenCode not installed" forever.2) Install verification —
wslCliInstallCommand()Before #47753 the install command was a plain
curl | bashwith no verification step. After #47753 it usesRemoteCli.installScript(), which appends:The command substitution and
$NFare consumed by the same pre-expansion pass;shactually receivestest "" = '2.0.2', so the install reports failure after the binary has already been installed successfully.Verification (same Node
child_process.spawncall the Electron main process uses)wsl -d Debian -- sh -lc <script>$HOME)/home/<user>/.opencode/bin/opencode✅RemoteCli.discoverScript()(post-#47753)Tested with WSL 2.6.1.0, Windows 11 build 26200, Debian (WSL2).
Suggested fix (targets this regression specifically)
packages/desktop/src/main/wsl/sidecar.tsalready uses), or--exec/-einwslArgs()so the arguments bypass the default Linux shell;RemoteCliscripts for SSH only, or teach them an "arguments are not expanded" mode.Happy to provide the full cmdline dumps or a repro script if useful.
gotenksIN commentedon Sep 19, 2026
I reproduced this on OpenCode Desktop 2.0.8 (Windows, stable) with bundled CLI 2.0.8, running on WSL 2.9.12.0 (kernel 6.18.40.1-1).
The test distro is Ubuntu with
/usr/bin/zshas its default shell, and the issue also reproduces with bash configured as the default shell.Inside WSL,
~/.opencode/bin/opencodereportsopencode v2.0.8, matching the desktop version.Settings → Ubuntu displays Install OpenCode, and the desktop log records:
Discovery fails before the version check runs, so this failure is not a version gate.
Minimal reproduction
The issue reproduces on Windows using PowerShell 7.6.6 with
$PSNativeCommandArgumentPassing = Windows:When called with
--, WSL runs the argument through the distro's default Linux shell.That outer shell expands
$clibefore the targetshprocess executes.The script remains a single argument, but its contents change.
A NUL-preserving
/proc/self/cmdlinecapture shows this expansion:Application mapping
RemoteCli.discoverScript()(viarunWslSh) arrives with$cliexpanded to empty, soresolveWslCli()returns empty andspawnWslSidecarreports not installed.RemoteCli.versionScript()survives both forms because it uses a quoted absolute path with no assignment-dependent interpolation.spawnWslSidecarsends its script over stdin (bash -se); that payload is unaffected, but it never runs because discovery fails first.$(), so it is exposed to the same pre-expansion.Harmless probes confirm the premature substitution; no installer was run.
Verification of the fix
Probes against WSL 2.9.12.0 show the behavior difference between
--and--exec:This verifies the discovery fix in PR #48796 (
wslArgs()changing from--to--exec).This test is not an end-to-end Desktop startup test because the probes used PowerShell/.NET argument lists.
A call-site audit found no regressions: explicit
sh -lc/bash -lccalls still interpret their own scripts, and only the unwanted outer-shell pass is removed.One caveat: bypassing the default shell also bypasses its startup-file environment changes, so a PATH/environment smoke test is worthwhile.
Related prior art: topgrade-rs/topgrade@c950a4e fixed another WSL argument-handling failure where Bash positional parameters were lost.
Its inline-and-quote workaround differs from the
--execfix verified here; that case did not involve argument splitting, and inline quoting is not automatically sufficient for these runtime-dependent scripts.AdiY00 commentedon Sep 19, 2026
Confirmed on OpenCode Desktop stable 2.0.8 and CLI 2.0.8 with Windows 11 build 26200.9457, WSL 2.7.14.0, and Arch Linux (default shell
/usr/bin/zsh).The CLI was installed globally with Bun and linked at
$HOME/.opencode/bin/opencode. Bothtest -xand--versionsucceed, but Desktop consistently reports OpenCode not installed.A Windows Node
child_process.spawn("wsl", ["-d", "archlinux", "--", "sh", "-lc", script])reproduction exits 0 with empty stdout.set -xconfirms the premature expansion:Passing
cli=/home/<user>/.opencode/bin/opencodefrom Windows withcli/uappended toWSLENV, then relaunching Desktop, immediately changes the distro status to Ready. This confirms the issue remains in 2.0.8 and also affects Arch WSL. No second CLI installation is required.gotenksIN commentedon Sep 19, 2026
It has already been fixed and released with 2.0.9, please update your v2 install.
Hona commentedon Sep 22, 2026
Fixed in #49902 (merged to
v2as e50d845, shipped in Desktop 2.0.9). WSL commands now run throughwsl.exe --execso the distro's login shell no longer pre-expands$VARand$(...)in the inline install and discovery scripts. Closing manually becausev2is not the default branch, so the PR did not auto-close it.fix(cli): bind a free service port when a WSL distro holds the default