anomalyco / anomalyco/opencode
[Desktop][WSL] 2.0.2: WSL detection and install verification always fail — wsl.exe re-expands $VAR in script arguments
@Hona is already working on this.
Since Sep 12, 2026.
- Dominant language
- TypeScript
- Stars
- 209k
- Forks
- 27.5k
- PR merge metrics
- PR metrics pending
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/opencode
Evidence
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"; fi
What 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' ""; fi
The 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
- #38309 (Desktop 1.18.x) was about only accepting the fixed
$HOME/.opencode/bin/opencodepath; PRs #44514 / #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"; fi
sh 实际收到:
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.
Contributor guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Assessment
This issue has not been assessed yet.