Repository navigation
Description
/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_SESSIONset) → insidetmux 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 fromC:\Windows, but the quoting bug breaks before
clip.exegets 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 CLINotes / what this is NOT
- Not OSC 52: the CLI does fire OSC 52 first, but its "copied to clipboard" UI
message is gated on theclip.exepath succeeding (when present), and the
user sees the failure error before any OSC-52 fallback messaging. - Not tmux:
tmux 3.4withset-clipboard external/offwill 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.exeshadowing: 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.
Activity
TheDr1ver commentedon May 27, 2026
Also worth mentioning that
/copyisn't the only problem - it also wasn't able to copy/paste anything out of the copilot CLI terminal via right-click or CTRL+C or CTRL+SHIFT+C until the patch was applied. I was unable to get anything to the clipboard from inside the copilot CLI while the bug was active.sinmentis commentedon Jun 11, 2026
Confirming this on a second architecture and environment — same root cause, same fix.
Environment
Confirmed root cause (unchanged from the original report)
In 1.0.61 the function is
Vcoinapp.js(duplicated insdk/index.js):The inner
"${e}"quotes get backslash-escaped by Node's Win32 spawn quoting as the argv crosses WSL interop, so cmd.exe receives\"C:\Windows\System32\clip.exe\", fails to find the command, and exits 1.Repro (x64, faithful to how the CLI spawns it)
The one-line fix proposed in this issue (drop the inner quotes) is verified working on x64. I applied it to both
app.jsandsdk/index.jsand/copynow succeeds.Additional finding — why this is fatal in some setups (likely the same root cause behind #3521)
writeToClipboard(Zxe) only emits the OSC 52 fallback when stdout is a TTY:On
clip.exefailure, the error is swallowed only if the OSC 52 path was taken. When stdout is not a TTY (e.g. running under a wrapper that pipes stdout),qvr()is false, OSC 52 is skipped, and the clip.exe error is rethrown as a hard failure. So:Fixing the quoting resolves the hard-failure case; separately, gating the surfaced error on "all backends failed" (rather than showing the first backend's error) would resolve #3521.
danmoseley commentedon Jun 11, 2026
https://github.com/github/copilot-agent-runtime/issues/10021
csmuell commentedon Jun 25, 2026
Confirming on 1.0.65, linux-x64 (WSL2 Ubuntu, Windows 11, inside tmux 3.4) -- still reproduces, same root cause and same one-line fix. In 1.0.65 the function is now
o_rinapp.js:Faithful repro (x64):
I can also confirm the TTY/OSC-52 nuance from the earlier comment: it surfaced as a hard error here, consistent with the OSC-52 fallback not being taken (
acceptsOsc()gating onstdout.isTTY). tmux is not involved in the failure -- the quoting breaks before clip.exe runs, regardless of tmux clipboard settings.Alternative workaround that survives
/updateThe
sed-patch approach has to be re-applied after every update (the bundle lives in a version-pinned~/.cache/copilot/pkg/.../<version>/dir). Since the CLI spawns the bare name"cmd.exe"on WSL (resolved viaPATH), you can instead drop a smallcmd.exeshim earlier onPATH(e.g.~/.local/bin, ahead of/mnt/c/Windows/System32). It intercepts only the exact clipboard call and delegates everything else to the realcmd.exe, so it's transparent and never needs re-applying:(Using
Set-Clipboardhere also sidesteps the UTF-8/BOM corruption reported in #2571 / #3062. The real fix is still the one-line de-quote in the CLI -- this is just a cleaner stopgap than patching the bundle.)TheDr1ver commentedon Jul 23, 2026
AI-ready quick win: still reproducible in 1.0.74-3
This remains unfixed in Copilot CLI 1.0.74-3 as of 2026-07-22. Every
/updatereinstalls the same broken expression, so affected WSL users must patch both generated bundles again. I just had to reapply the exact workaround toapp.jsandsdk/index.js.The functional fix remains one expression:
A cleaner source-level fix would spawn
clip.exedirectly and remove the unnecessarycmd.exequoting layer.Why this is an unusually strong quick-win / major quality-of-life fix:
/copyand terminal copy affordances, and can become a hard failure when OSC 52 is unavailable.AI-ready, one-cycle acceptance criteria:
clip.exeinvocation so nested quotes cannot be corrupted through WSL interop.linux-arm64andlinux-x64WSL paths.Please consider labeling this good first issue, help wanted, or quick win, or assigning it directly to a Copilot coding agent. This is exactly the kind of tightly scoped standing bug that even the smallest available coding-agent tier should be able to resolve end-to-end in one cycle.
andricicezar commentedon Sep 10, 2026
Confirming this remains reproducible in the current release, Copilot CLI 1.0.83, on WSL2 x86_64 as of September 10, 2026.
Environment
1.0.83linux-x64numtide/llm-agents.nixThis is not caused by Nix sandboxing or tmux. The exact command constructed by the CLI fails when reproduced independently:
Output:
Exit status:
1.Both alternatives work:
The generated
app.jsin 1.0.83 still contains:Changing it to the unquoted form fixes
/copyand interactive selection copying. One packaging nuance: the Linux distribution includes a Node SEA executable with the JavaScript embedded in the binary, so modifying only the adjacentapp.jshas no effect unless the patched bundle is run through Node or the executable itself is rebuilt.The latest-release reproduction confirms the original one-line diagnosis and proposed fix are still valid on x86_64.
patrick-5546 commentedon Sep 24, 2026
Confirming this remains reproducible in Copilot CLI 1.0.88 on WSL2 x86_64 with tmux 3.4.
cmd.exe /c versucceeds, so WSL interop is healthy. Both cached 1.0.88 bundles still contain:`chcp 65001 >nul & "${n}"`Patching both
app.jsandsdk/index.jsto:`chcp 65001 >nul & ${n}`and restarting Copilot fixes
/copy. The corrected command exits 0. Enabling tmuxset-clipboarddid not resolve theclip.exeexit-code failure, confirming that tmux/OSC 52 is separate from the underlying quoting bug.credmond commentedon Oct 7, 2026
2026-10-07T09:57:07.194Z [WARNING] writeToClipboard: clip.exe failed; relied on OSC 52: Error: clip.exe exited with code 1
Not on WSL. This is just Cmd or PowerShell. Going on for many many months now. Please fix, Microsoft.
copilot-cli-bot commentedon Nov 11, 2026
This appears to be resolved as of GitHub Copilot CLI v1.0.109. Please update to that release or a newer stable version. We're closing this as completed. If it still happens, please comment with your version and reproduction steps.