Skip to content

Sync failed on Windows #88

Description

@Moha-Master

Generated by AI and reviewed by human

Environment

  • OS: Windows 11 (win32)
  • opencode-synced: 0.11.0 (also reproduced on main @ src/shell-node.ts)
  • opencode: v2.0.18 (v2 plugin API → uses createNodeShell())
  • gh CLI: installed and authenticated (gh auth status OK)

Summary

Since #81 (feat: support opencode v2 plugin API alongside v1, released as v0.11.0), the v2 entrypoint runs all git/gh commands through the new shell-node.ts shim. That shim quotes interpolated values with POSIX single quotes, but on Windows child_process.exec spawns cmd.exe, which does not strip single quotes. As a result gh/git receive the quotes as literal characters and every sync command that shells out to them fails.

This breaks pull, push and /sync-link on Windows for any config with
includeSecrets / includeMcpSecrets / includeSessions / includePromptStash enabled,
because ensureSensitiveSyncPolicy() → ensureRepoPrivate() runs first.

Reproduction

// src/shell-node.ts
function shellQuote(value) {
  return `'${String(value).replaceAll("'", `'\\''`)}'`;   // POSIX style
}
node -e "
const {exec} = require('child_process');
exec(\"gh repo view '<owner>/<repo>' --json isPrivate\", (e, out, err) => {
  console.log('failed:', !!e); if (e) console.log(String(err).trim());
});
"

Actual output on Windows:

failed: true
GraphQL: Could not resolve to a Repository with the name ''<owner>/<repo>''. (repository)

Note the doubled quotes ''<owner>/<repo>'' — cmd.exe passed the single quotes through verbatim.

The resulting plugin error:

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)

The same call works fine when quoted for cmd.exe, and works with execFile:

# double quotes → OK
node -e "require('child_process').exec('gh repo view \"<owner>/<repo>\" --json isPrivate', (e,o)=>console.log(e?'FAIL':'OK '+o))"

# execFile (no shell at all) → OK
node -e "require('child_process').execFile('gh',['repo','view','<owner>/<repo>','--json','isPrivate'],(e,o)=>console.log(e?'FAIL':'OK '+o))"

Both variants were verified on the same machine.

Why v0.10.1 was unaffected

Before #81 the v1 entrypoint used Bun's $ template tag, which handles Windows quoting itself. The comment in shell-node.ts documents the assumption explicitly:

Runs via child_process.exec (/bin/sh), not bash; keep commands POSIX.

That assumption only holds on POSIX hosts.

Suggested fix

Preferred — drop the shell entirely and pass an argv array (also removes any quoting/injection ambiguity):

import { execFile } from 'node:child_process';

const execFileAsync = promisify(execFile);

const shell = (strings, ...values) => {
  const parts = strings.reduce(
    (acc, s, i) => (i ? [...acc, { raw: s }, values[i - 1]] : [s]), []); // split literal/interpolated
  // literals → single command name + raw args; interpolated values → argv entries
  ...
};

Simpler — make quoting platform-aware:

function shellQuote(value: unknown): string {
  const s = String(value);
  if (process.platform === 'win32') {
    return `"${s.replaceAll('"', '""')}"`;
  }
  return `'${s.replaceAll("'", `'\\''`)}'`;
}

The second option restores v0.10.x behaviour but keeps cmd.exe metacharacter exposure (&, %, ^), so execFile/spawn without a shell is the more robust long-term fix.

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

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions