Skip to content

WSL2 (ARM64): /copy fails with clip.exe exited with code 1 due to cmd.exe quoting in 1.0.55 #3534

Description

@TheDr1ver

/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.

Activity

  1. TheDr1ver commented on May 27, 2026

    @TheDr1ver
    Author

    Also worth mentioning that /copy isn'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.

  2. added
    area:platform-windowsWindows-specific: PowerShell, cmd, Git Bash, WSL, Windows Terminal
    area:input-keyboardKeyboard shortcuts, keybindings, copy/paste, clipboard, mouse, and text input
    on May 27, 2026
  3. added theissue type on May 27, 2026
  4. sinmentis commented on Jun 11, 2026

    @sinmentis

    Confirming this on a second architecture and environment — same root cause, same fix.

    Environment

    • Copilot CLI 1.0.61 (also seen on 1.0.60), Node v24, linux-x64 (original report was arm64)
    • WSL2 Ubuntu on a Windows 365 Cloud PC, accessed over RDP
    • Reproduces both standalone and inside a wrapper that pipes stdout (non-TTY)

    Confirmed root cause (unchanged from the original report)

    In 1.0.61 the function is Vco in app.js (duplicated in sdk/index.js):

    function Vco(t){
      let e = gV() ? Gco() : jco(),                 // e -> "C:\Windows\System32\clip.exe"
          r = gV() ? "cmd.exe" : process.env.ComSpec || "cmd.exe";
      return new Promise((n,o)=>{
        let s = Xc(zco(r,["/d","/c",`chcp 65001 >nul & "${e}"`]));  // <-- inner quotes
        // ...
      });
    }

    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)

    # WITH inner quotes (what the CLI does) -> FAILS
    printf x | /mnt/c/Windows/System32/cmd.exe /d /c 'chcp 65001 >nul & "C:\Windows\System32\clip.exe"'; echo $?
    #   '"C:\Windows\System32\clip.exe"' is not recognized ...   -> 1
    
    # WITHOUT inner quotes -> WORKS
    printf x | /mnt/c/Windows/System32/cmd.exe /d /c 'chcp 65001 >nul & C:\Windows\System32\clip.exe'; echo $?
    #   -> 0

    The one-line fix proposed in this issue (drop the inner quotes) is verified working on x64. I applied it to both app.js and sdk/index.js and /copy now 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:

    function qvr(){ return process.stdout.isTTY === true && process.env.TERM !== "dumb"; }

    On clip.exe failure, 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.

  5. csmuell commented on Jun 25, 2026

    @csmuell

    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_r in app.js:

    function o_r(t){
      let e = Nk() ? r_r() : n_r(),                 // e -> "C:\Windows\System32\clip.exe"
          n = Nk() ? "cmd.exe" : process.env.ComSpec || "cmd.exe";
      return new Promise((r,o)=>{
        let s = ob(t_r(n,["/d","/c",`chcp 65001 >nul & "${e}"`]));  // <-- inner quotes
        ...

    Faithful repro (x64):

    # WITH inner quotes (what the CLI does) -> FAILS, exit 1
    printf x | /mnt/c/Windows/System32/cmd.exe /d /c 'chcp 65001 >nul & "C:\Windows\System32\clip.exe"'; echo $?
    #   '"C:\Windows\System32\clip.exe"' is not recognized ...  -> 1
    
    # WITHOUT inner quotes -> WORKS, exit 0
    printf x | /mnt/c/Windows/System32/cmd.exe /d /c 'chcp 65001 >nul & C:\Windows\System32\clip.exe'; echo $?
    #   -> 0

    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 on stdout.isTTY). tmux is not involved in the failure -- the quoting breaks before clip.exe runs, regardless of tmux clipboard settings.

    Alternative workaround that survives /update

    The 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 via PATH), you can instead drop a small cmd.exe shim earlier on PATH (e.g. ~/.local/bin, ahead of /mnt/c/Windows/System32). It intercepts only the exact clipboard call and delegates everything else to the real cmd.exe, so it's transparent and never needs re-applying:

    cat > ~/.local/bin/cmd.exe <<'SH'
    #!/bin/bash
    REAL_CMD="/mnt/c/Windows/System32/cmd.exe"
    if [[ "$#" -eq 3 && "$1" == "/d" && "$2" == "/c" && "$3" == *clip.exe* && "$3" == *chcp* ]]; then
      payload="$(cat)"
      if command -v powershell.exe >/dev/null 2>&1; then
        printf '%s' "$payload" | powershell.exe -NoProfile -NonInteractive \
          -Command '$in=[Console]::In.ReadToEnd(); Set-Clipboard -Value $in' && exit 0
      fi
      printf '%s' "$payload" | /mnt/c/Windows/System32/clip.exe
      exit $?
    fi
    exec "$REAL_CMD" "$@"
    SH
    chmod +x ~/.local/bin/cmd.exe

    (Using Set-Clipboard here 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.)

  6. TheDr1ver commented on Jul 23, 2026

    @TheDr1ver
    Author

    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 /update reinstalls the same broken expression, so affected WSL users must patch both generated bundles again. I just had to reapply the exact workaround to app.js and sdk/index.js.

    The functional fix remains one expression:

    - `chcp 65001 >nul & "${e}"`
    + `chcp 65001 >nul & ${e}`

    A cleaner source-level fix would spawn clip.exe directly and remove the unnecessary cmd.exe quoting layer.

    Why this is an unusually strong quick-win / major quality-of-life fix:

    • Clipboard access is a core CLI interaction, not an edge feature.
    • The failure affects /copy and terminal copy affordances, and can become a hard failure when OSC 52 is unavailable.
    • The root cause, faithful reproduction, exact fix, and cross-architecture confirmation are already documented here.
    • The implementation is tiny, deterministic, and needs no product or design decision.
    • A regression test can assert the generated argv or exercise the faithful WSL command shape.

    AI-ready, one-cycle acceptance criteria:

    1. Fix the shared source that constructs the clip.exe invocation so nested quotes cannot be corrupted through WSL interop.
    2. Add a regression test covering the failing quoted shape and successful corrected shape.
    3. Ensure both CLI and SDK build outputs inherit the source fix rather than patching generated bundles independently.
    4. Verify linux-arm64 and linux-x64 WSL 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.

  7. andricicezar commented on Sep 10, 2026

    @andricicezar

    Confirming this remains reproducible in the current release, Copilot CLI 1.0.83, on WSL2 x86_64 as of September 10, 2026.

    Environment

    • Copilot CLI: 1.0.83
    • Architecture: linux-x64
    • WSL distro: Ubuntu
    • Host terminal: Alacritty on Windows
    • Multiplexer: tmux 3.4
    • Installation: Nix package from numtide/llm-agents.nix

    This is not caused by Nix sandboxing or tmux. The exact command constructed by the CLI fails when reproduced independently:

    printf x | /mnt/c/Windows/System32/cmd.exe /d /c \
      'chcp 65001 >nul & "C:\Windows\System32\clip.exe"'

    Output:

    '\"C:\Windows\System32\clip.exe\"' is not recognized as an internal or external command,
    operable program or batch file.
    

    Exit status: 1.

    Both alternatives work:

    # Invoke clip.exe directly.
    printf x | /mnt/c/Windows/System32/clip.exe
    
    # Or omit the nested quotes.
    printf x | /mnt/c/Windows/System32/cmd.exe /d /c \
      'chcp 65001 >nul & C:\Windows\System32\clip.exe'

    The generated app.js in 1.0.83 still contains:

    spawn(cmd, ["/d", "/c", `chcp 65001 >nul & "${clip}"`])

    Changing it to the unquoted form fixes /copy and 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 adjacent app.js has 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.

  8. patrick-5546 commented on Sep 24, 2026

    @patrick-5546

    Confirming this remains reproducible in Copilot CLI 1.0.88 on WSL2 x86_64 with tmux 3.4. cmd.exe /c ver succeeds, so WSL interop is healthy. Both cached 1.0.88 bundles still contain:

    `chcp 65001 >nul & "${n}"`

    Patching both app.js and sdk/index.js to:

    `chcp 65001 >nul & ${n}`

    and restarting Copilot fixes /copy. The corrected command exits 0. Enabling tmux set-clipboard did not resolve the clip.exe exit-code failure, confirming that tmux/OSC 52 is separate from the underlying quoting bug.

  9. credmond commented on Oct 7, 2026

    @credmond

    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.

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

    area:input-keyboardKeyboard shortcuts, keybindings, copy/paste, clipboard, mouse, and text inputarea:platform-windowsWindows-specific: PowerShell, cmd, Git Bash, WSL, Windows Terminal

    Type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions