Skip to content

fix(agents): bin path override + envOverride + Windows shell launch#17

Merged
lefarcen merged 2 commits into
mainfrom
fix/win32-spawn-bin-override-13-15
May 15, 2026
Merged

fix(agents): bin path override + envOverride + Windows shell launch#17
lefarcen merged 2 commits into
mainfrom
fix/win32-spawn-bin-override-13-15

Conversation

@joeylee12629-star

Copy link
Copy Markdown
Contributor

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).

Issue Reporter Symptom Fix
#15 @LuckyFishGeek Windows EINVAL ("无效的参数") on every Convert / Draft shell: true on win32 only, so npm-installed .cmd agent shims can launch
#13 @hallestar Auto-detection picks the wrong binary; no UI to override; Scoop on Windows not covered Custom-path field in Settings + invocation layer honors override / env / PATH; userToolchainDirs() adds Scoop layouts

Two commits, ordered surface-down → engine-up so review reads naturally:

  1. feat(settings): UI for custom agent binary path + Scoop autodetect (#13) — Settings UI, store schema (v5 → v6), client-side wire-through, Scoop directory coverage.
  2. 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 .cmd shim on Windows. Node's child_process.spawn cannot launch a .cmd without going through the shell. macOS / Linux exec the binary directly so the same code path worked there.

Fixshell: process.platform === "win32" in the spawn options. 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 --model value comes from a curated list, not free-form user input. So flipping shell: true does 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.ts defines envOverride per agent (e.g. CLAUDE_BIN) but invokeAgent() ignored it — it re-resolved via resolveOnPath() only.
  • The Settings UI had no input for a custom path. Once auto-detection picked the wrong binary, the user was stuck.
  • userToolchainDirs() did not cover Scoop-managed Node, where global npm shims land in C:\Users\<user>\scoop\apps\nodejs\current\ (no /bin/ subdirectory).

Three pieces in this PR:

  1. Bin resolver in invoke.ts. New resolveBinForAgent() 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).
    • PATH scan over def.bin then def.fallbackBins.
  2. 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.

  3. 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 (default C:\ProgramData\scoop)
    • %APPDATA%\npm (default standalone-installer location)
    • Existing NPM_CONFIG_PREFIX now 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. useConvert and useDraft read the override for the current agent and ship it as binOverride on the /api/convert and /api/draft request bodies.

i18n. Six new agent.customBin.* keys (en + zh-CN).


Verified

End-to-end with Playwright + a real browser session:

  • Settings → Claude Code → "Custom path" panel renders with eyebrow / subtitle / auto-detected path / placeholder hint / Save / Clear.
  • Type /no/such/path/claude → Save → localStorage.html-everything-storestate.agentBinOverrides = { claude: "/no/such/path/claude" }.
  • Convert / Draft with that override → event: error with "Claude Code: custom path \/no/such/path/claude` does not exist. Update or clear it in Settings → Custom path."`
  • Clear button → store clears → next Convert / Draft falls back to PATH scan and works normally.
  • pnpm exec tsc --noEmit clean; pnpm build clean.

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

  • On Windows 11, with Claude Code installed via Scoop / npm-on-Scoop, Convert and Draft both work without EINVAL.
  • Settings → any installed agent → Custom path panel renders. The auto-detected path is shown.
  • Filling in an absolute path that exists → Save → Convert uses that binary (visible in the Run log's spawn … line).
  • Filling in an absolute path that does not exist → Convert immediately surfaces the actionable error.
  • Clear → falls back to auto-detection.
  • Existing users (v5 store) → no migration error; agentBinOverrides initialized to {}.

Out of scope

  • Multi-binary fallback chain UI ("try this path, then this path, then PATH"). Single override field is enough for the reported failure modes; can be extended later if needed.
  • Auto-test of the binary on save (run <bin> --version and warn if it errors). Same — can layer on later; the actionable error on Convert is already enough signal.
  • Generalizing 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

joey and others added 2 commits May 15, 2026 12:03
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
lefarcen merged commit fec591b into main 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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

windows10 环境有问题 提示无效的参数 Agent binary path auto-detection is incorrect; no way to manually override

2 participants