Skip to content

server: unpinned plugin update check flashes visible git console window on Windows #50868

Closed

Description

@sugu6

Summary

On Windows, the shared background service (opencode serve --service) runs without a console. When it cold-starts and checks an unpinned git plugin for updates, it spawns git ls-remote without CREATE_NO_WINDOW, so Windows opens a new visible console window (routed through the default terminal application, Windows Terminal) that shows git output on the desktop. Because the spawner is the shared service, the window appears whether the client is the TUI or the Desktop app — it reads as "a git terminal window pops up when OpenCode starts."

Environment

  • opencode version: 2.0.15
  • OS: Microsoft Windows [版本 10.0.22631.6199] (Windows 11 23H2, x64); default terminal application: Windows Terminal 1.24.11911.0
  • Terminal: Windows Terminal; TERM=xterm-256color, TERM_PROGRAM / COLORTERM unset
  • Shell: %ComSpec% = C:\Windows\system32\cmd.exe (interactive shell: PowerShell 7.6.6 in Windows Terminal)
  • Install/channel: npm global install (%APPDATA%\npm\node_modules\@opencode\cli), release channel; auto-updated 2.0.14 → 2.0.15 during the captured occurrence
  • Active plugins: superpowers@git+https://github.com/obra/superpowers.git (unpinned git spec, global config), local ~/.config/opencode/plugins/rtk.ts

Reproduction

  1. Install OpenCode via npm and configure an unpinned git plugin: "plugin": ["superpowers@git+https://github.com/obra/superpowers.git"].
  2. Make sure no background service is running (fresh boot, idle exit, or — as observed — a version update that restarts the service).
  3. Launch any client (TUI or Desktop) so it cold-starts the shared service.
  4. Within ~5 seconds of service start, a window titled C:\Program Files\Git\cmd\git.exe appears on the desktop running git ls-remote https://github.com/obra/superpowers.git; when that attempt fails, a second window appears with git ls-remote ssh://[email protected]/....

Expected Behavior

Service child processes should spawn with CREATE_NO_WINDOW, consistent with the shell-tool pwsh spawns and snapshot git spawns (which are already hidden) — no visible console windows.

Actual Behavior

A visible Windows Terminal window with git content flashes on every service cold start. Window lifetime = ls-remote latency: brief when GitHub is fast, ~27 seconds when GitHub is slow/unreachable, after which the check fails with NpmInstallFailedError: An unknown git error occurred, preceded by an ssh-retry window.

Additional Context

Runtime capture (EnumWindows + IsWindowVisible probe; local time = UTC+8; local paths redacted):

15:37:03.204 WINDOW pid=17476 class=CASCADIA_HOSTING_WINDOW_CLASS title=C:\Program Files\Git\cmd\git.exe
    owner-chain: WindowsTerminal.exe(17476)
15:37:04.294 WINDOW pid=6268 class=PseudoConsoleWindow
    owner-chain: git.exe(6268): git --no-replace-objects ls-remote https://github.com/obra/superpowers.git
      << opencode.exe(1988): ...\@opencode\cli\bin\opencode.exe serve --service
      << opencode.exe(7244): ...\@opencode\cli\bin\opencode.exe
      << pwsh.exe(11052): C:\Program Files\PowerShell\7\pwsh.exe
15:37:25.262 WINDOW pid=17844 class=PseudoConsoleWindow
    owner-chain: git.exe(17844): git --no-replace-objects ls-remote ssh://[email protected]/obra/superpowers.git
      << opencode.exe(1988): ...\opencode.exe serve --service
      << opencode.exe(7244)
      << pwsh.exe(11052)

Service log (verbatim):

timestamp=2026-09-23T07:36:17.611Z level=INFO run=b3ea6081 message="update check" current=2.0.14 latest=2.0.15 role=cli
timestamp=2026-09-23T07:36:29.226Z level=INFO run=b3ea6081 message="spawning process" command=npm args=["install","--global","--force","@opencode/[email protected]"] role=cli
timestamp=2026-09-23T07:37:00.335Z level=INFO run=7bc8a43d ... (service cold start, new version)
timestamp=2026-09-23T07:37:28.114Z level=WARN run=7bc8a43d message="failed to check plugin update" target=superpowers@git+https://github.com/obra/superpowers.git cause="Cause([Fail(NpmInstallFailedError (cause: Error: An unknown git error occurred))])" http.span=27335 role=server

Timeline: 07:36:17 update detected → 07:36:29 npm install (runs inside the client's console — no flash) → 07:37:00 service cold start → 07:37:03 and 07:37:25 visible git windows → 07:37:28 plugin check fails after http.span=27335 ms.

Contrast — same service process, spawns that are hidden correctly (an 8-minute visible-window monitoring session covering 3 snapshot git bursts and dozens of 5-second git polls from an MCP daemon captured zero windows from these paths):

  • snapshot git: --git-dir <data>/snapshot/<hash> ... diff-files / write-tree / ...
  • MCP daemon git polls every ~5 s (codebase-memory-mcp)
  • shell-tool pwsh -NoLogo -NoProfile -NonInteractive ... spawns

So the missing window-hiding flag appears specific to the plugin-update-check call site (and possibly other cold-start git call sites such as rev-parse / worktree list, which run in the same window).

Reproducibility: intermittent — only when the service cold-starts (update day, first launch of the day), and only noticeable while ls-remote is slow. Observed twice on 2026-09-23 (04:16:22 and 07:37:28 UTC), both right after service cold starts.

Documented workaround (not applied in this environment): pinning the plugin to a full commit hash (superpowers@git+...#<40-char-sha>) skips the update check entirely ("full Git commit hashes stay pinned"). Suggested fixes:

  1. Pass CREATE_NO_WINDOW when spawning plugin-update-check git, consistent with the other spawn sites.
  2. Consider skipping or deferring the ls-remote check on cold start when offline — today it blocks ~27 s, fails, and only produces visible windows.

Activity

  1. dayesann commented on Oct 14, 2026

    @dayesann

    Additional data from 2.0.29 (desktop app, Windows 10 19045, git 2.54.0) confirming this and making it reproducible on demand rather than only on a cold start.

    On-demand reproduction (no cold start needed)

    opencode api post /api/plugin/check      # POST /api/plugin/check
    

    With an unpinned git plugin (superpowers@git+https://github.com/obra/superpowers.git) the request takes ~6.9 s, and a window exists for exactly that interval (EnumWindows + IsWindowVisible, ConsoleWindowClass, sampling every 150 ms; local time UTC+8):

    23:26:17.90  POST issued
    23:26:18.20  APPEAR  pid=35484  proc=git  title=[C:\Program Files\Git\cmd\git.exe]
    23:26:24.68  GONE
    23:26:24.59  response returned
    

    So it is not a flash: the window lifetime equals the git operation and it stays visible while ls-remote retries/fails (here NpmInstallFailedError: An unknown git error occurred), which is why it reads as a stuck popup. In the same 90 s sampling window, ~12 snapshot/diff git invocations produced zero windows.

    Why it recurs on every launch, not only after a cold start

    PluginUpdate.check caches its result in an in-memory map with a 24 h TTL, so the cache dies with the service. The desktop app starts the service together with the app, so the git check runs on every launch.

    Code path in the bundled CLI, for the fix

    • @opencode/AppProcess passes windowsHide: !0 for StandardCommand spawns (and the kill path uses pr.exec("taskkill ...", { windowsHide: !0 })), so snapshot/shell children are hidden.
    • Npm.check resolves with { preferOnline: true, noGitRevCache: true, ignoreScripts: true } and, for type === "git", goes through the bundled npm git code (@npmcli/git), which shells out to git without any window-hiding flag. That spawn is the one that gets a fresh visible console because the service has no console. It is also visible in the logs: only the AppProcess spawns emit "spawning process" lines, the failing plugin check does not.

    Why pinning to a commit works, quantified

    Npm.check early-returns for immutable specs (if (!a.mutable) return false), so a full 40-hex committish never invokes git at all. Measured after pinning both git plugins to their currently installed commits:

    • check duration 6.9 s -> 0.3-0.6 s
    • no git process spawned, zero console windows over a 90 s sampling window
    • both plugins reinstall once into a new cache dir (one git clone) because the cache dir name is derived from the spec, then stay quiet

    Suggested fix remains the same: give this npm-git spawn the same windowsHide / CREATE_NO_WINDOW treatment as AppProcess.

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

Metadata

Metadata

Assignees

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