[STG-2279] fix(cli): quote win32 npx spawn + bound skills installer stages#2250
[STG-2279] fix(cli): quote win32 npx spawn + bound skills installer stages#2250shrey150 wants to merge 4 commits into
Conversation
- Quote command + args with cmd.exe-safe quoting when spawning .cmd/.bat shims through shell:true, so the default 'C:\Program Files\nodejs\npx.cmd' and install paths with spaces survive Node's unquoted join into 'cmd.exe /d /s /c "..."'. - Give the npx child a 180s deadline (SIGTERM then SIGKILL) and surface skill_install_timeout through the existing fail/resultCode plumbing. - Bound catalog/file fetches with AbortSignal.timeout(10s), preserving the catalog-unavailable fallback semantics on abort. - Both timeouts are env-overridable (BROWSE_SKILLS_INSTALL_TIMEOUT_MS, BROWSE_SKILLS_FETCH_TIMEOUT_MS) following the module's BROWSE_SKILLS_* pattern, which also makes the deadlines testable end-to-end. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
🦋 Changeset detectedLatest commit: 2fa7df9 The changes in this PR will be included in the next version bump. This PR includes changesets to release 0 packagesWhen changesets are added to this PR, you'll see the packages that this PR includes changesets for and the associated semver types Not sure what this means? Click here to learn what changesets are. Click here if you're a maintainer who wants to add another changeset to this PR |
There was a problem hiding this comment.
No issues found across 3 files
Confidence score: 5/5
- Automated review surfaced no issues in the provided summaries.
- No files require special attention.
Architecture diagram
sequenceDiagram
participant User as CLI User
participant CLI as browse CLI
participant Spawn as spawnPassthrough()
participant Child as npx Child Process
participant FS as Filesystem (npx.cmd/.sh)
participant Catalog as Catalog API (HTTP)
participant Blob as Blob Store (HTTP)
Note over CLI,Blob: NEW: Spawn Quoting (Windows fix)
User->>CLI: browse skills add <skill-id>
CLI->>CLI: resolve npx via PATH+PATHEXT
CLI->>FS: findExecutable("npx")
FS-->>CLI: "C:\Program Files\nodejs\npx.cmd" (win32)
alt win32 + .cmd/.bat shim
CLI->>CLI: quoteForCmdShell(command) + quoteForCmdShell(args)
Note over CLI: Wraps space-containing tokens in double-quotes
CLI->>Spawn: spawnPassthrough(quotedCommand, quotedArgs, timeoutMs)
Spawn->>Spawn: shell: true (required for .cmd)
Spawn->>Spawn: set deathTimer = setTimeout(timeoutMs)
Spawn->>Child: cmd.exe /d/s/c ""C:\Program Files\nodejs\npx.cmd" --yes skills add ..."
else darwin/linux or no .cmd
CLI->>Spawn: spawnPassthrough(command, args, timeoutMs)
Spawn->>Spawn: set deathTimer = setTimeout(timeoutMs)
Spawn->>Child: /usr/local/bin/npx --yes skills add ...
end
Note over Spawn,Child: NEW: 180s install deadline (env BROWSE_SKILLS_INSTALL_TIMEOUT_MS)
alt Child completes normally before deadline
Child-->>Spawn: exit code 0
Spawn-->>CLI: { exitCode: 0, timedOut: false }
CLI->>CLI: install succeeded
CLI-->>User: exit 0 + success message
else Child exits with error
Child-->>Spawn: exit code != 0
Spawn-->>CLI: { exitCode: N, timedOut: false }
CLI->>CLI: fail with error + resultCode: "skill_install_failed"
CLI-->>User: exit 1 + error
else Child hangs past timeout
Spawn->>Spawn: deathTimer fires
Spawn->>Child: SIGTERM
Spawn->>Spawn: set killTimer = setTimeout(5s, SIGKILL)
Spawn-->>CLI: { exitCode: 1, timedOut: true }
CLI->>CLI: fail with message + resultCode: "skill_install_timeout"
CLI-->>User: exit 1 + timeout message
end
Note over CLI,Catalog: NEW: 10s catalog fetch deadline (env BROWSE_SKILLS_FETCH_TIMEOUT_MS)
CLI->>Catalog: GET /skills/<id>/files (signal: AbortSignal.timeout(10s))
alt Catalog responds before deadline
Catalog-->>CLI: file list JSON
CLI->>Blob: HEAD /skills/<id>/SKILL.md (signal: AbortSignal.timeout(10s))
Blob-->>CLI: 200 OK
CLI->>Blob: GET /skills/<id>/<file> (signal: AbortSignal.timeout(10s))
Blob-->>CLI: file bytes
CLI->>CLI: proceed with install flow
else Catalog or Blob timeout
Catalog--xCLI: AbortError (timeout)
CLI->>CLI: treat as "unavailable" (same as network failure)
Note over CLI: Falls back to direct GitHub installer
CLI->>Child: npx skills add ... (direct install)
end
Win32 validation (one-time windows-latest run)Closes the Windows verification gap called out in the E2E matrix. A throwaway workflow ( Run (green): https://github.com/browserbase/stagehand/actions/runs/27448084696
Runner layout (both jobs)
Before —
|
| Suite | Result | Notes |
|---|---|---|
Win32-relevant gate: vitest run tests/skills-install.test.ts -t "quoteForCmdShell|spawnPassthrough|uses a shell for Windows command shims" |
10 passed, 3 skipped | All quoting tests, shim detection, and both spawnPassthrough timeout tests (incl. "kills a hung child after the deadline", 522ms) pass natively on Windows — this also covers the install-timeout behavior, so no separate BROWSE_SKILLS_INSTALL_TIMEOUT_MS smoke was needed. |
Full CLI suite: vitest run |
205 passed, 17 failed, 2 skipped (224) | All 17 failures are pre-existing POSIX assumptions in the test harness, not product code: 9 skills/skills-install tests stub npx as an extension-less #!/bin/sh script (unresolvable via PATHEXT on Windows → "npx is not installed") or exec /bin/sleep; the other 8 (cli-functions-contract ×3, cli-templates ×3, driver-foundation ×1, package-manifest ×1) have similar host-tooling/path assumptions. Same-by-construction on main and this PR. |
Net
Same runner, same Node layout, same command: main fails with the exact predicted 'C:\Program' is not recognized error; this PR installs the skill end-to-end. The Windows row in the E2E matrix is updated to point at this run.
ajmcquilkin
left a comment
There was a problem hiding this comment.
I have no idea if this works on Windows or not, I would want to see platform matrix E2E tests in CI to validate that this install works
Runs the platform-specific quoteForCmdShell/spawnPassthrough/shell-shim tests on windows-latest (gated on packages/cli changes). The repo already runs windows-latest jobs for SEA builds; this is the first cross-OS *test* leg. Scoped to the cross-platform-clean subset — the rest of the CLI suite still has POSIX-only harness assumptions. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Replaces the brittle CI test-name filter with the codebase's existing platform idiom (it.runIf(process.platform !== 'win32') in driver-foundation.test.ts), centralized as itPosix/describePosix in tests/helpers/platform.ts. The win32 job now runs the same test:cli as ubuntu; POSIX-only tests (shell-script stubs) opt out locally. Coverage is on by default — new tests run on Windows unless they explicitly skip. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The full-suite Windows run surfaced 5 more POSIX-dependent tests: functions publish/build/scaffold (sh-script npm stubs), the alive-unresponsive daemon test (unix-socket path), and the package-manifest test (npm pack of the Linux-built manifest — a build-env check, not a user path). All guarded with itPosix. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Summary
Linear: https://linear.app/browserbase/issue/STG-2279/fix-windows-skills-add-npx-quoting-and-bound-installer-timeouts
Fixes
browse skills addon Windows (cmd.exe spawn quoting) and bounds the two unbounded skills-installer stages (thenpx skills addchild and the catalog/file fetches).Impact if merged
skills.add succeeds for 3.2% of Windows users (54 in the last 30d) — effectively broken for every default
C:\Program Files\nodejsNode install, because the npx child is spawned through cmd.exe with an unquoted path. Windows devs are a smaller slice of the CLI base, but skill installers are the single highest-value cohort in our telemetry: 7x the engagement (median 12.5 vs 2 commands) and 19x the multi-day retention (28.4% vs 1.5%) of non-installers, and skills.find→add is an agent-facing funnel (51% of finders attempt an install within an hour). This also bounds the two unbounded installer stages (npx child: 180s; catalog fetches: 10s) — today they can hang forever, feeding the slow-failure retry loops that dominate telemetry volume.Implementation notes
Root cause.
findExecutableresolvesnpxvia PATH+PATHEXT tonpx.cmdon Windows, andspawnPassthroughspawns it withshell: true(required for.cmd/.batshims). Node'sshell: truejoins command+args unquoted intocmd.exe /d /s /c "...", soC:\Program Files\nodejs\npx.cmdsplits at the space and cmd executesC:\Program→'C:\Program' is not recognized→ exit 1. Install-path args underC:\Users\<First Last>\...break the same way.Why not
shell: false. Spawning a.cmddirectly withshell: falsethrowsEINVALon all current Node versions — the CVE-2024-27980 hardening (Node 18.20.x / 20.12.x / 21.7.x+) forbids spawning batch files without a shell because cmd.exe argument splitting cannot be made injection-safe generically. So the shell path is mandatory for.cmdshims, and the args must be quoted for cmd.Quoting semantics.
quoteForCmdShellwraps tokens containing whitespace, quotes, or cmd metacharacters (^ & | < >) in double quotes, doubling embedded quotes. Node wraps the joined string in outer quotes after/d /s /c; with/s, cmd strips only those outer quotes and executes the inner, correctly-quoted line:Alternative considered. Resolving
npx-cli.jsnext tonpx.cmdand spawningprocess.execPathwithshell: falsewould avoid cmd quoting entirely, but thenpx.cmd→npx-cli.jsrelative layout differs across npm versions and Node distribution channels (nvm-windows, Volta, Scoop shims, fnm), so it trades a well-understood quoting rule for fragile path archaeology. The quoting approach is smaller and matches what cross-platform tools (e.g.cross-spawn) do.Bounding the installer stages.
spawnPassthroughnow enforces a 180s deadline: SIGTERM, then SIGKILL after 5s if the child ignores it. A timed-out install fails with a clear message and a distinctskill_install_timeoutresult code through the existingfail/resultCodeplumbing from [STG-2192] fix(cli): make browse skills add failures diagnosable + fail cleanly on unknown skill #2210.AbortSignal.timeout(10s). An aborted catalog fetch is classified exactly like a network failure (unavailable), preserving the existing fallback semantics.BROWSE_SKILLS_INSTALL_TIMEOUT_MS,BROWSE_SKILLS_FETCH_TIMEOUT_MS), following the module's existingBROWSE_SKILLS_*override pattern; this is also what makes the deadlines provable end-to-end in tests.E2E Test Matrix
All commands ran against the locally built CLI (
<local build>/bin/run.js) on macOS (darwin/arm64).windows-latestbefore/after run: actions/runs/27448084696)C:\Program Files\nodejs(no setup-node),where npx→C:\Program Files\nodejs\npx.cmd. main:browse skills install→ exit 1, verbatim'C:\Program' is not recognized as an internal or external command. PR head d2e9098: exit 0,Installed 1 skill,~\.agents\skills\browse\SKILL.mdpresent; win32 vitest gate (quoting + shim + spawnPassthrough timeout) 10/10 passed.browse skills find flights(real catalog)google.com/search-flights-ts4g1fwith full metadatabrowse skills add google.com/search-flights-ts4g1f(real catalog, realnpx)Downloaded 2 skill files to <config dir>;npx skills addinstalled the skill ("Installed 1 skill ... Done!")C:\Program Files\nodejs\npx.cmd(unit tests + helper output)C:\Program Files\nodejs\npx.cmd --yes skills add C:\Users\First Last\...(unquoted → cmd runsC:\Program); after:"C:\Program Files\nodejs\npx.cmd" --yes skills add "C:\Users\First Last\..."& | ^ < >metachars, and the empty token. Static proof only — see Windows row.npxstub (exec /bin/sleep 600) +BROWSE_SKILLS_INSTALL_TIMEOUT_MS=2000→browse skills installSkill install timed out after 2s waiting for \npx skills add`...`skill_install_timeoutflows through the samefailplumbing verified in #2210). Also covered byspawnPassthroughunit tests (timeout + non-timeout control).browse skills add google.com/search-flights-ts4g1fwith stubbednpx--yes skills add browserbase/browse.sh --skill google.com/search-flights-ts4g1fBROWSE_SKILLS_FETCH_TIMEOUT_MS=500.npx vitest run(packages/cli)pnpm lint(packages/cli)Windows gap closed: a one-time
windows-latestbefore/after run (actions/runs/27448084696, details in the validation comment) reproduced the exact'C:\Program' is not recognizedfailure on main and verifiedbrowse skills installsucceeds end-to-end on this PR's build with the defaultC:\Program Files\nodejssystem Node. Full Windows vitest: 205/224 passed; all 17 failures are pre-existing POSIX test-harness assumptions (#!/bin/shnpx stubs etc.), identical by construction on main.🤖 Generated with Claude Code
Summary by cubic
Fixes Windows failures in
browse skills addby quoting thenpxcommand when spawned via cmd.exe, bounds installer/fetch stages to prevent hangs, and runs the full CLI test suite on Windows. Addresses Linear: STG-2279..cmd/.batthrough the shell, soC:\Program Files\nodejs\npx.cmdand paths with spaces work.npx skills add(SIGTERM, then SIGKILL) and a 10s abort for catalog/file fetches; both overridable viaBROWSE_SKILLS_INSTALL_TIMEOUT_MSandBROWSE_SKILLS_FETCH_TIMEOUT_MS; install timeouts surfaceskill_install_timeout.windows-latest); POSIX-only tests are now guarded viaitPosix/describePosixso Windows gets full coverage without brittle CI filters.Written for commit 2fa7df9. Summary will update on new commits.