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.
Generated by AI and reviewed by human
Environment
win32)0.11.0(also reproduced onmain@src/shell-node.ts)v2.0.18(v2 plugin API → usescreateNodeShell())gh auth statusOK)Summary
Since #81 (
feat: support opencode v2 plugin API alongside v1, released as v0.11.0), the v2 entrypoint runs allgit/ghcommands through the newshell-node.tsshim. That shim quotes interpolated values with POSIX single quotes, but on Windowschild_process.execspawnscmd.exe, which does not strip single quotes. As a resultgh/gitreceive the quotes as literal characters and every sync command that shells out to them fails.This breaks
pull,pushand/sync-linkon Windows for any config withincludeSecrets/includeMcpSecrets/includeSessions/includePromptStashenabled,because
ensureSensitiveSyncPolicy()→ensureRepoPrivate()runs first.Reproduction
Actual output on Windows:
Note the doubled quotes
''<owner>/<repo>''—cmd.exepassed the single quotes through verbatim.The resulting plugin error:
The same call works fine when quoted for
cmd.exe, and works withexecFile: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 inshell-node.tsdocuments the assumption explicitly: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):
Simpler — make quoting platform-aware:
The second option restores v0.10.x behaviour but keeps
cmd.exemetacharacter exposure (&,%,^), soexecFile/spawnwithout a shell is the more robust long-term fix.