Skip to content

fix: resolve $ template argv on Windows instead of a shell string - #89

Open
markmybytes wants to merge 2 commits into
iHildy:mainfrom
markmybytes:fix/windows-shell-argv
Open

markmybytes wants to merge 2 commits into
iHildy:mainfrom
markmybytes:fix/windows-shell-argv

Conversation

@markmybytes

@markmybytes markmybytes commented Oct 13, 2026 •

Copy link
Copy Markdown

Summary

Fixes #88, fixes #91, fixes #93

Since the v2 plugin entrypoint (#81), every git/gh command from the plugin runs through the Node shell shim in src/shell-node.ts. That shim interpolated values with POSIX single-quote escaping ('...') and executed the result via child_process.exec — which on Windows spawns cmd.exe. cmd.exe does not treat single quotes as argument delimiters, so gh/git received quotes literally:

Unable to verify repo visibility: Command failed: gh repo view '<owner>/<repo>' --json isPrivate
GraphQL: Could not resolve to a Repository with the name ''<owner>/<repo>''. (repository)

This broke pull, push, and /sync-link on Windows for any config that reaches ensureRepoPrivate() (includeSecrets / includeMcpSecrets / includeSessions / includePromptStash). v0.10.1 was unaffected because the v1 entrypoint used Bun's $ template tag, which does its own Windows argv handling.

Fix

Drop the shell instead of trying to quote for it — the approach the issue calls preferred, since platform-aware quoting keeps discipline exposed to cmd.exe metacharacters:

  • buildArgv tokenizes the $-tagged template into an argv array with Bun $ semantics: static text splits on whitespace, each ${} glues into the current token as one argument (origin/${branch}..HEAD stays one element), and an empty value keeps its argv slot (git commit -m ${''} does not collapse to a bare -m, which would hang in an interactive editor).
  • The template now spawns via execFile with no shell, so no platform quoting rules apply at all.
  • The shell fallback only remains for cases Node cannot spawn shell-less: .cmd/.bat shims (refused since CVE-2024-27980) and bare PATHEXT-resolved names (ENOENT). Those re-run as a one-liner through exec, platform-quoted — MSVCRT list2cmdline rules (quoteWin32) on Windows, POSIX single-quote on the rest. The fallback is documented as re-entering a shell, so its PATH resolution and %…% expansion ceiling differ from a direct spawn.
  • Because there is no shell anymore, &&, pipes, redirects, and globs in template text now arrive as literal argv elements rather than operators. No existing call site uses any of them (verified by scan); the behavior difference is documented in the shim's header for future call sites.

Follow-up: hide spawned console windows

Spawns now pass windowsHide: true. On Windows the plugin host runs without an
attached console, so each git child previously flashed a conhost window —
one flash per git call per sync. Matches how opencode's core spawners already
hide children (cross-spawn-spawner.ts passes windowsHide on win32).

child_process.exec defaults to cmd.exe on Windows, which treats POSIX
single quotes as literal characters. Every interpolated git call
(git -C <path> ...) passed quote characters to git and failed with
fatal: cannot change to ''<path>'', breaking pull, push, status, and
commit on Windows entirely.

Run the tokenized command via execFile with no shell. Static text
splits on whitespace; interpolated values glue to adjacent text so
mid-token interpolations (origin/${branch}..HEAD, --max-count=${n})
stay a single argv element, and empty values keep their argv slot so
'commit -m ""' cannot collapse to a bare -m and hang in an editor.
Fall back to a quoted shell one-liner only when the executable is a
.cmd/.bat shim (EINVAL since CVE-2024-27980); that path retains the
cmd.exe percent-expansion and newline ceilings.

All 37 git/gh/op call-site templates use no shell features, so this
is a drop-in replacement that also removes command-injection surface
for commit messages and repo paths.
@markmybytes
markmybytes force-pushed the fix/windows-shell-argv branch from 0920374 to 2ce387e Compare October 13, 2026 04:14
child_process defaults to windowsHide: false; when the plugin host has
no attached console, every console-subsystem child (git) flashes a conhost
window. Pass windowsHide: true on both the execFile path and the .cmd/.bat
shell fallback, matching opencode's own spawners
(packages/core/src/cross-spawn-spawner.ts).
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

1 participant