fix(agents): bin path override + envOverride + Windows shell launch#17
Merged
Conversation
Reported by @hallestar (issue #13). When auto-detection picks the wrong binary — or fails outright on a Scoop-managed Node install on Windows — users had no way to manually correct it. This adds: - **Settings UI**: each installed agent card now has a "Custom path" panel underneath the model picker. Empty = use auto-detection. Filled with an absolute path = override. Save / Clear buttons; Enter / Esc shortcuts; auto-detected path is shown next to the input as a reference. - **Persistence**: `agentBinOverrides: Record<string, string>` lives on the persisted store with a v5 → v6 migration that initializes it to `{}` on existing browsers. Empty values are pruned by the setter. - **Wire-through**: `useConvert` and `useDraft` read the override for the current agent and ship it as `binOverride` on the `/api/convert` and `/api/draft` request bodies. - **Scoop coverage**: `userToolchainDirs()` now scans Scoop layouts (`%SCOOP%\shims`, `%SCOOP%\apps\nodejs\current`, the matching global install paths) and `%APPDATA%\npm`. Together with the existing `NPM_CONFIG_PREFIX` check (now also scanning the prefix dir directly, not just `<prefix>/bin` — npm on Windows lays out shims one level up) this covers the "I installed Node via Scoop and Claude Code is in C:\Users\me\scoop\apps\nodejs\current" case raised in the issue. i18n: 6 new `agent.customBin.*` keys (en + zh-CN). The matching engine-side change — actually honoring `binOverride` and `process.env[envOverride]` during invocation — is in the next commit. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
, #15) Two related invocation-layer fixes that were both blocking real users: **Issue #15 — Windows EINVAL on .cmd shims** (reported by @LuckyFishGeek) Every CLI agent we support is installed by npm as a `.cmd` shim on Windows. Node's `child_process.spawn` cannot launch a `.cmd` without going through the shell, so every Convert / Draft run was failing immediately with `EINVAL` ("无效的参数"). Set `shell: process.platform === "win32"` so macOS/Linux behavior is unchanged. Safety: the prompt travels via stdin or `--message <text>` (argv-message), never interpolated into a shell command line, so this does not introduce a shell-injection vector. **Issue #13 — bin override + env never honored** (reported by @hallestar) Detection found the right path but invocation re-resolved via PATH only. Replace the inlined `resolveOnPath` loop with a typed `resolveBinForAgent` that tries, in order: 1. `opts.binOverride` (the new custom-path field from the previous commit's Settings UI). If this is set but does not exist, **hard-fail with a clear error** ("custom path X does not exist; update or clear it in Settings → Custom path") instead of silently falling through to PATH. The user picked an explicit path; they deserve to see the typo. 2. `process.env[def.envOverride]` (e.g. `CLAUDE_BIN`, `OPENCLAW_BIN` — defined in `detect.ts` but previously read only during detection, never during invocation). 3. PATH scan over `def.bin` then `def.fallbackBins`. Verified via curl against /api/draft: - `binOverride: "/no/such/path/claude"` → `event: error` with the actionable message. - omitted → resolves via PATH as before. - `CLAUDE_BIN=/path/to/claude` env var → wins over PATH. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
lefarcen
approved these changes
May 15, 2026
This was referenced May 15, 2026
joeylee12629-star
added a commit
that referenced
this pull request
May 15, 2026
… on reload + #16 follow-up (#22) * fix(ux): consolidate Convert into a single coral chip The Toolbar carried its own Convert/Stop button (and the ⌘/Ctrl+Enter shortcut) while a floating ConvertChip rendered the same action over the editor / preview divider. Two buttons, identical action — confusing. Drop the toolbar pair, leave the chip as the single source of Convert truth, and migrate the keyboard shortcut onto it. Recolor the chip from the previous near-black `var(--ink)` to `var(--coral)` (idle) / `var(--coral-hover)` (running) so the primary CTA is visually distinct from the muted toolbar surface and from text content. Disabled state still renders coral at 0.4 opacity. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(home): detect agents on mount so the agent chip restores after reload The store persists `selectedAgent` across hard reloads but not the `agents[]` list. Today that list is only populated when Settings or Welcome modals open and call `/api/agents`. So a user who reloads without opening either modal sees the toolbar agent chip render the red "Select agent" fallback even though their selection is intact — because `agents.find(a => a.id === selectedAgent)` returns undefined against an empty list. Fire the same `/api/agents` fetch from the home page on mount (after hydration, so it doesn't fight SSR). Failures fall through silently — Settings / Welcome retain their own retry-on-open paths. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(agents): use shell on win32 in resolveOpenclawAgentId (#16 follow-up) PR #17 fixed the win32 spawn EINVAL in `invokeAgent` but missed the matching `spawn` inside `resolveOpenclawAgentId`. The same root cause applies — npm-installed openclaw on Windows is a `.cmd` shim that Node cannot launch directly without `shell: true`. Same Unix behavior, no shell-injection surface (argv is a fixed `["agents", "list"]`, not user input). Closes #16 (the second of two locations identified in the original report). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This was referenced May 15, 2026
Closed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #13
Closes #15
Summary
Two reported issues, one PR — both live in the agent-invocation layer and the fixes touch the same files (
detect.ts,invoke.ts).EINVAL("无效的参数") on every Convert / Draftshell: trueon win32 only, so npm-installed.cmdagent shims can launchuserToolchainDirs()adds Scoop layoutsTwo commits, ordered surface-down → engine-up so review reads naturally:
feat(settings): UI for custom agent binary path + Scoop autodetect (#13)— Settings UI, store schema (v5 → v6), client-side wire-through, Scoop directory coverage.fix(agents): respect bin override + envOverride; use shell on win32 (#13, #15)— invocation-layer bin resolver + the win32 spawn fix.#15 — Windows spawn EINVAL
Root cause — npm installs every supported CLI agent (Claude Code, Cursor Agent, Gemini CLI, ...) as a
.cmdshim on Windows. Node'schild_process.spawncannot launch a.cmdwithout going through the shell. macOS / Linux exec the binary directly so the same code path worked there.Fix —
shell: process.platform === "win32"in thespawnoptions. Unix is unchanged.Safety — the prompt travels via stdin (stdin protocol) or
--message <text>(argv-message protocol), never interpolated into a shell command line. The--modelvalue comes from a curated list, not free-form user input. So flippingshell: truedoes not open up shell-injection.Diagnosed and proposed by @LuckyFishGeek in #15.
#13 — Manual binary path override + Scoop on Windows
Root cause analysis (from @hallestar in the issue):
detect.tsdefinesenvOverrideper agent (e.g.CLAUDE_BIN) butinvokeAgent()ignored it — it re-resolved viaresolveOnPath()only.userToolchainDirs()did not cover Scoop-managed Node, where global npm shims land inC:\Users\<user>\scoop\apps\nodejs\current\(no/bin/subdirectory).Three pieces in this PR:
Bin resolver in
invoke.ts. NewresolveBinForAgent()returns one of{ ok | override-missing | not-found }. Tries, in order:opts.binOverride(Settings UI). If set but does not exist, hard-fail with a clear error instead of silently falling through. Users who typed a path deserve to see the typo, not run a different binary by accident.process.env[def.envOverride](e.g.CLAUDE_BIN).def.binthendef.fallbackBins.Settings UI ("Custom path" panel). Each installed agent card now has a custom-path panel below the model picker. Empty = use auto-detection. Filled with an absolute path = override. Save / Clear buttons; Enter / Esc shortcuts; the auto-detected path is shown next to the input as a reference.
Scoop coverage in
userToolchainDirs(). New scan paths for Windows:%SCOOP%\shims,%SCOOP%\apps\nodejs\current,%SCOOP%\apps\nodejs-lts\current%SCOOP_GLOBAL%\shims,%SCOOP_GLOBAL%\apps\nodejs\current(defaultC:\ProgramData\scoop)%APPDATA%\npm(default standalone-installer location)NPM_CONFIG_PREFIXnow also scans the prefix dir directly, not just<prefix>/bin— npm on Windows installs shims one level up.Persistence.
agentBinOverrides: Record<string, string>lives on the persisted store with a v5 → v6 migration that initializes it to{}on existing browsers. Empty values are pruned by the setter.Wire-through.
useConvertanduseDraftread the override for the current agent and ship it asbinOverrideon the/api/convertand/api/draftrequest bodies.i18n. Six new
agent.customBin.*keys (en + zh-CN).Verified
End-to-end with Playwright + a real browser session:
/no/such/path/claude→ Save →localStorage.html-everything-store→state.agentBinOverrides = { claude: "/no/such/path/claude" }.event: errorwith"Claude Code: custom path \/no/such/path/claude` does not exist. Update or clear it in Settings → Custom path."`pnpm exec tsc --noEmitclean;pnpm buildclean.Win32 shell-launch fix is by inspection — does not regress macOS (PR was developed on macOS arm64 and continues to work). @LuckyFishGeek's repro instructions match the fix; once this lands they can re-test on Windows 11 + Scoop.
Test plan for reviewers
EINVAL.spawn …line).agentBinOverridesinitialized to{}.Out of scope
<bin> --versionand warn if it errors). Same — can layer on later; the actionable error on Convert is already enough signal.HTML_ANYTHING_AGENT_PROXY/ extra-agent env hooks (this is what PR fix: harden agent conversion and localize fonts #14 covers, separately).🤖 Generated with Claude Code