Skip to content

v2: execSync("git ...") spawns a visible cmd.exe window on Windows and breaks IME input #101

Description

@leonvchan

Summary

On Windows, the code the bundle labels src/services/tags.ts queries git with
execSync(<string command>) and does not pass windowsHide: true. Node runs a
string command through cmd.exe on Windows, and windowsHide defaults to
false, so that child is created with a console window. Everything this plugin
runs inside the OpenCode service process, and on this machine console processes
spawned from that process demonstrably do get visible windows (see Actual
Behavior). The resulting window takes foreground focus, which breaks IME
composition while a session is in use.

Environment

  • opencode version: v2.0.21 (latest)
  • OS: Windows 11 25H2, build 26200.8655 (win32 x64). Note: the version API still
    reports 10.0.26200 because Windows 11 kept the 10.0 major/minor; build >= 22000
    is what identifies Windows 11.
  • Terminal: OpenCode desktop app (Electron)
  • Shell: C:\WINDOWS\system32\cmd.exe
  • Install/channel: release (latest)
  • Active plugins: opencode-supermemory 2.0.15, superpowers (git)

Reproduction

  1. Run OpenCode v2 on Windows with this plugin enabled, inside a git repository.
  2. Send a message and let the agent take a turn.
  3. Console windows appear repeatedly, observed at roughly one per second, titled
    C:\WINDOWS\system32\cmd.exe.

Expected Behavior

The plugin's internal git queries for tags/context should never create a
visible window.

Actual Behavior

With a 60 ms process/window watcher running during an active session, over a
133 second span (19:15:11 - 19:17:24):

    162 x cmd
    161 x conhost
    145 x git
     10 x node

That is roughly one cmd.exe + conhost.exe pair per second, frequently with
git children. In total 105 new visible ConsoleWindowClass windows were
recorded in that 133 s span — about one every 1.3 seconds. Newly created
visible windows in the same capture (window class ConsoleWindowClass):

19:16:56.644 | class=ConsoleWindowClass title='C:\WINDOWS\system32\cmd.exe'
19:16:57.140 | class=ConsoleWindowClass title='C:\WINDOWS\system32\cmd.exe'
19:16:57.687 | class=ConsoleWindowClass title='C:\WINDOWS\system32\cmd.exe'
19:16:59.181 | class=ConsoleWindowClass title='C:\WINDOWS\system32\cmd.exe'
19:17:00.288 | class=ConsoleWindowClass title='C:\WINDOWS\system32\cmd.exe'
19:17:01.743 | class=ConsoleWindowClass title='C:\WINDOWS\system32\cmd.exe'

and one titled after a git binary:

19:15:46.517 | class=ConsoleWindowClass title='C:\Program Files\Git\cmd\git.exe'

That last line is the important one for this report: it shows that a console
process spawned from the OpenCode service process on this machine is created
with a visible window — which is exactly the mechanism described above.

Capture method: WMI Win32_ProcessStartTrace could not be subscribed on this
machine (Call cancelled), so this is polling based and processes shorter than
the 60 ms interval may be undercounted.

Impact: with a Chinese IME (Google Pinyin) the repeated focus stealing makes the
candidate window flicker and typing produce nothing.

Caveat on attribution, stated plainly: in the same capture, OpenCode's own MCP
server startup also spawns cmd.exe /d /s /c "npx ...", and those children
include node.exe and git-remote-https.exe. The MCP servers were disabled at
the same time as this patch was applied, so this capture cannot establish how
much of the observed flashing came from the plugin specifically. The missing
windowsHide is nonetheless a defect on its own: on Windows it creates a
console window whenever this code runs in a process without a console.

Additional Context

The offending calls (dist/v2/index.js). stdio does not suppress a console
window on Windows — windowsHide: true is what does:

    const gitCommonDir = execSync("git rev-parse --git-common-dir", {
      cwd: directory,
      encoding: "utf-8",
      stdio: ["pipe", "pipe", "pipe"]
    }).trim();

dist/v2/index.js contains six such call sites (git rev-parse --git-common-dir, git rev-parse --show-toplevel x3, git config user.email,
git remote get-url origin). The same execSync(...) + options pattern also
appears in dist/cli.js, dist/index.js and dist/server.js.

The CLI already passes the option, which suggests an oversight rather than
intent:

execFile(command, args, { windowsHide: true }, (error) => { ... });

Two smaller observations:

  • getGitRoot() is not memoized (repoInfoCache only covers
    getGitRepoInfo()), so it shells out to git on every call.
  • execSync with a string command also routes through cmd.exe, adding a
    process layer that execFileSync("git", [...]) would avoid.

Suggested fix: pass windowsHide: true in those options objects (and/or switch
to execFileSync with an argv array).

Workaround applied locally: windowsHide: true added to all six call sites in
dist/v2/index.js. After that change, a 20 second watch during a turn showed a
single cmd.exe, not visible.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions