/copy fails on WSL2 (ARM64): clip.exe exited with code 1 — quoting bug in cmd.exe wrapper
Summary
In Copilot CLI 1.0.55-1 running under WSL2 (Ubuntu, ARM64 / aarch64),
every clipboard write through the Windows path fails with:
Failed to copy to clipboard: Error: clip.exe exited with code 1
Triggered by /copy, "copy" affordances in the scroll view, /session id, and
anything else routed through writeToClipboard() → writeToClipExe().
The bug is in how the CLI quotes the clip.exe path inside the cmd.exe
wrapper, not in clip.exe, not in WSL interop generally, and not in the
user's terminal/tmux setup. clip.exe itself works fine when invoked directly.
Environment
- WSL:
WSL version 2.7.3.0, Kernel 6.6.114.1-1, WSLg 1.0.73
- Distro: Ubuntu (
/home/driver/...)
- Host: Windows 11 on ARM64
- Architecture:
aarch64 (CLI install dir: ~/.cache/copilot/pkg/linux-arm64/1.0.55-1/)
- Terminal: Windows Terminal (
WT_SESSION set) → inside tmux 3.4
- Copilot CLI:
1.0.55-1
- Current working directory at time of failure: under
/home/driver/..., which
translates to a UNC path \\wsl.localhost\Ubuntu\home\driver\...
Note: CMD.EXE warns "UNC paths are not supported. Defaulting to Windows
directory." on every invocation. That's noise — not the root cause. The
wrapper still runs from C:\Windows, but the quoting bug breaks before
clip.exe gets a chance.
Root cause
In app.js (and the same code in sdk/index.js), writeToClipExe() builds
the command as:
function mMs(t) {
let e = OU() ? fMs() : pMs(), // resolves to "C:\Windows\System32\clip.exe"
r = OU() ? "cmd.exe" : process.env.ComSpec || "cmd.exe";
return new Promise((n, o) => {
let s = _A(dMs(r, ["/d", "/c", `chcp 65001 >nul & "${e}"`]));
// ^^^^^^
// embedded double-quotes around the path
...
});
}
So the spawn args are effectively:
cmd.exe /d /c 'chcp 65001 >nul & "C:\Windows\System32\clip.exe"'
When Node's child_process.spawn ships that third argv element to Windows via
WSL interop, the embedded " characters get backslash-escaped (the standard
Win32 CommandLineToArgvW quoting rules applied by Node's spawn). By the time
CMD parses the /c string, it sees:
chcp 65001 >nul & \"C:\Windows\System32\clip.exe\"
CMD does not recognize \" as a quote escape, so it tries to execute a program
literally named \"C:\Windows\System32\clip.exe\", which doesn't exist, and
exits with code 1. That bubbles up as the user-visible error.
Reproduction
Inside WSL2 (Ubuntu, ARM64), with CWD anywhere under /home/...:
# Direct: works
echo "hello" | /mnt/c/Windows/System32/clip.exe
echo "exit=$?" # exit=0 ✓
# CMD wrapper without inner quotes: works
echo "hello" | /mnt/c/Windows/System32/cmd.exe /d /c \
'chcp 65001 >nul & C:\Windows\System32\clip.exe'
echo "exit=$?" # exit=0 ✓
# CMD wrapper WITH inner quotes (what the CLI does today): FAILS
echo "hello" | /mnt/c/Windows/System32/cmd.exe /d /c \
'chcp 65001 >nul & "C:\Windows\System32\clip.exe"'
echo "exit=$?"
# stderr: '"C:\Windows\System32\clip.exe"' is not recognized as an internal
# or external command, operable program or batch file.
# exit=1 ✗
Tested 100% reproducible on this box across:
- ASCII / empty / UTF-8 / 50 KB payloads — all fail identically
- With and without
chcp 65001 >nul & prefix
- Direct
cmd.exe /d /c 'echo interop_works' (no embedded quotes) succeeds, so
general interop is healthy
Proposed fix
Drop the inner double-quotes — they're protecting against a path with spaces,
but C:\Windows\System32\clip.exe has none, and (more importantly) the System32
clip.exe path is the only thing this wrapper ever invokes:
- let s = _A(dMs(r, ["/d", "/c", `chcp 65001 >nul & "${e}"`]));
+ let s = _A(dMs(r, ["/d", "/c", `chcp 65001 >nul & ${e}`]));
(Source location: function mMs() in the minified app.js; same code is
duplicated in sdk/index.js.)
If protection against future paths-with-spaces is desired, the correct
WSL-safe shape is to drop the wrapper entirely and spawn('clip.exe', [])
directly — that path also works in my testing and avoids the cmd.exe quoting
hazard altogether. The chcp 65001 prefix is a no-op for clip.exe anyway
since the binary is fed UTF-16-friendly text via stdin, and Node's spawn
with windowsHide: true already inherits the parent code page.
If the chcp 65001 matters for some other reason I'm missing, an alternative
that still works around the quoting bug is to invoke via start /b or use a
single argv element with no nested quoting.
Workaround for affected users
Patch the local install (will need to be re-applied after /update):
CLI_DIR=~/.cache/copilot/pkg/linux-arm64/1.0.55-1 # adjust for your version
for f in "$CLI_DIR/app.js" "$CLI_DIR/sdk/index.js"; do
cp "$f" "$f.bak-clipfix"
sed -i 's|chcp 65001 >nul & "${e}"|chcp 65001 >nul & ${e}|' "$f"
done
# then restart the CLI
Notes / what this is NOT
- Not OSC 52: the CLI does fire OSC 52 first, but its "copied to clipboard" UI
message is gated on the clip.exe path succeeding (when present), and the
user sees the failure error before any OSC-52 fallback messaging.
- Not tmux:
tmux 3.4 with set-clipboard external/off will additionally
swallow OSC 52, but that's a separate (and well-known) issue with its own
fix (set -g set-clipboard on; set -g allow-passthrough on; set -as terminal-features ',*:clipboard').
- Not
clip.exe shadowing: the changelog entry "Clipboard copy works correctly
on Windows when non-system clip.exe shadows the system one in PATH" suggests
the CLI already hardcodes the System32 path (fMs() / pMs()). That's
correct; the bug is purely the quoting around it.
- Not architecture-specific in principle, but I can only reproduce on
linux-arm64; an x86_64 WSL2 box may behave the same since the quoting
logic is identical — would appreciate confirmation from someone with an
AMD64 WSL2 setup.
/copyfails on WSL2 (ARM64):clip.exe exited with code 1— quoting bug incmd.exewrapperSummary
In Copilot CLI 1.0.55-1 running under WSL2 (Ubuntu, ARM64 /
aarch64),every clipboard write through the Windows path fails with:
Triggered by
/copy, "copy" affordances in the scroll view,/session id, andanything else routed through
writeToClipboard()→writeToClipExe().The bug is in how the CLI quotes the
clip.exepath inside thecmd.exewrapper, not in
clip.exe, not in WSL interop generally, and not in theuser's terminal/tmux setup.
clip.exeitself works fine when invoked directly.Environment
WSL version 2.7.3.0,Kernel 6.6.114.1-1,WSLg 1.0.73/home/driver/...)aarch64(CLI install dir:~/.cache/copilot/pkg/linux-arm64/1.0.55-1/)WT_SESSIONset) → insidetmux 3.41.0.55-1/home/driver/..., whichtranslates to a UNC path
\\wsl.localhost\Ubuntu\home\driver\...Root cause
In
app.js(and the same code insdk/index.js),writeToClipExe()buildsthe command as:
So the spawn args are effectively:
When Node's
child_process.spawnships that third argv element to Windows viaWSL interop, the embedded
"characters get backslash-escaped (the standardWin32
CommandLineToArgvWquoting rules applied by Node's spawn). By the timeCMD parses the
/cstring, it sees:CMD does not recognize
\"as a quote escape, so it tries to execute a programliterally named
\"C:\Windows\System32\clip.exe\", which doesn't exist, andexits with code 1. That bubbles up as the user-visible error.
Reproduction
Inside WSL2 (Ubuntu, ARM64), with CWD anywhere under
/home/...:Tested 100% reproducible on this box across:
chcp 65001 >nul &prefixcmd.exe /d /c 'echo interop_works'(no embedded quotes) succeeds, sogeneral interop is healthy
Proposed fix
Drop the inner double-quotes — they're protecting against a path with spaces,
but
C:\Windows\System32\clip.exehas none, and (more importantly) the System32clip.exe path is the only thing this wrapper ever invokes:
(Source location: function
mMs()in the minifiedapp.js; same code isduplicated in
sdk/index.js.)If protection against future paths-with-spaces is desired, the correct
WSL-safe shape is to drop the wrapper entirely and
spawn('clip.exe', [])directly — that path also works in my testing and avoids the cmd.exe quoting
hazard altogether. The
chcp 65001prefix is a no-op forclip.exeanywaysince the binary is fed UTF-16-friendly text via stdin, and Node's
spawnwith
windowsHide: truealready inherits the parent code page.If the
chcp 65001matters for some other reason I'm missing, an alternativethat still works around the quoting bug is to invoke via
start /bor use asingle argv element with no nested quoting.
Workaround for affected users
Patch the local install (will need to be re-applied after
/update):Notes / what this is NOT
message is gated on the
clip.exepath succeeding (when present), and theuser sees the failure error before any OSC-52 fallback messaging.
tmux 3.4withset-clipboard external/offwill additionallyswallow OSC 52, but that's a separate (and well-known) issue with its own
fix (
set -g set-clipboard on; set -g allow-passthrough on; set -as terminal-features ',*:clipboard').clip.exeshadowing: the changelog entry "Clipboard copy works correctlyon Windows when non-system clip.exe shadows the system one in PATH" suggests
the CLI already hardcodes the System32 path (
fMs()/pMs()). That'scorrect; the bug is purely the quoting around it.
linux-arm64; an x86_64 WSL2 box may behave the same since the quotinglogic is identical — would appreciate confirmation from someone with an
AMD64 WSL2 setup.