[BUG] Text copied from Claude Code TUI doesn't cross RDP clipboard redirection; pasting into Notepad first is required

Status Open
Reported on v2.1.220
Maintainer reply None cached
Activity 4 comments · opened Jul 25, 2026

Environment

  • Claude Code: 2.1.220 (issue began when the mouse-enabled TUI rolled out)
  • Server: Windows Server 2025 Standard (10.0.26100), Windows Terminal, PowerShell
  • Client: Windows laptop connected via Remote Desktop (mstsc) with clipboard redirection enabled

Description

Since the "mouse enabled" version of the Claude Code TUI, text copied from Claude Code running in Windows Terminal on an RDP server no longer reaches the clipboard of the RDP client machine. Pasting on the laptop yields nothing (or stale content from an earlier copy).

The copy does succeed locally on the server: pasting into Notepad on the server works. Re-copying that same text from Notepad then propagates to the laptop normally. So the round-trip workaround is: copy in Claude Code → paste into server-side Notepad → re-copy → paste on laptop.

Expected behavior

Text copied from the Claude Code TUI should behave like any other clipboard content and be picked up by RDP clipboard redirection (rdpclip), the same way a copy from Notepad or Windows Terminal's native selection is.

Suspected cause

The symptom pattern (works locally, invisible to rdpclip, fixed by laundering through Notepad; at times associated with a still-running Claude Code instance holding clipboard ownership) suggests Claude Code is placing the text on the clipboard via OLE with delayed rendering, or holding clipboard ownership in a way rdpclip cannot serialize for redirection. A plain immediate CF_UNICODETEXT set (which is what Notepad produces) redirects fine.

Steps to reproduce

  1. RDP from a Windows laptop into a Windows server with clipboard redirection enabled.
  2. On the server, run Claude Code 2.1.220 in Windows Terminal (mouse mode active).
  3. Select and copy text from the Claude Code TUI.
  4. On the laptop, try to paste → nothing / stale content.
  5. On the server, paste into Notepad (works), copy from Notepad, paste on the laptop → works.

What was ruled out

  • Restarting rdpclip.exe on the server and reconnecting the RDP session: no change.
  • Extensive general clipboard troubleshooting (assumed "bad clipboard"): no change. The failure is specific to copies originating from the Claude Code TUI.

Related issues

  • #72617 (copy from TUI doesn't reach system clipboard on Windows 10 — local-only variant of this)
  • #66056 (mouse reporting capturing right-click since ~2.1.167)

View original on GitHub ↗

3 Comments

did-vmonroig · 2 months ago

Same thing here on 2.1.220, with "tui": "fullscreen". Windows 11 Pro 26200, Windows Terminal 1.24.11911.0. Happens with both mRemoteNG and plain mstsc.exe, so it isn't the client.

Worth noting it's not just copy-on-select: I turned that off in /config and it still fails. Any copy done by the TUI is affected, including select + Ctrl+C. The only thing that works is holding Shift while selecting, so Windows Terminal does the selection itself, then Ctrl+C. You can tell the two apart at a glance — Claude Code's selection is light blue, the terminal's own is reverse video.

I dumped the native clipboard formats after each kind of copy:

  • Shift + native terminal selection: UnicodeText, Locale, Text, OEMText → pastes fine on the remote
  • Claude Code TUI selection: UnicodeText only → never reaches the remote

That's the same missing CF_LOCALE/CF_OEMTEXT signature described in #67576, so I think it's the same bug, but OpenConsole isn't sufficient to avoid it, and the fix suggested there — native clipboard writes instead of OSC 52 for local Windows sessions — looks like the right call.

Ruled out before posting: restarting rdpclip in the remote session, mstsc vs mRemoteNG, server-side redirection (works fine in both directions from anything else), UIPI (nothing elevated on either side), clipboard history and cbdhsvc, copyFormatting (already "none"), and encoding — same failure with plain ASCII and with accented text. Writing the same string from a separate process with Set-Clipboard crosses over instantly, so the channel itself is healthy; it's specifically the TUI's clipboard write that never gets announced to the server.

fletchgqc · 2 months ago

I got claude to write a workaround script which does the trick for me. Here is the script and additional info Claude produced:

Environment

| | |
|---|---|
| Claude Code | 2.1.222 |
| OS | Windows 11 Pro 10.0.26200 (RDP session, mstsc client) |
| Terminal | Windows Terminal 1.24.11911.0 (current stable) |
| Console host | OpenConsole.exe — Claude Code runs in a tab spawned inside WT (explorer.exe → WindowsTerminal.exe → pwsh → claude.exe) |

Symptom matches this issue exactly: copy in the Claude Code TUI → paste inside the RDP session works, paste on the RDP client yields nothing. Laundering through Notepad fixes it.

Short version of the finding: the TUI's OSC 52 write ends up on the clipboard through Windows Terminal's OLE path, which adds the DataObject and Ole Private Data container formats. rdpclip then transfers nothing at all — not even the perfectly valid CF_UNICODETEXT sitting right next to them. Measurements in the last section.

---

1. What would actually fix this

Use a native Win32 clipboard write on local Windows sessions instead of OSC 52. This was already suggested in #67576 and it is the real fix: OSC 52 is only needed when the clipboard is remote (SSH). A direct OpenClipboard / EmptyClipboard / SetClipboardData(CF_UNICODETEXT, …) produces exactly the format set that rdpclip transfers without complaint — verified below, that is precisely what makes the Notepad workaround work.

Worth noting this is not fixable on the Windows Terminal side any time soon: WT's main has already been rewritten to raw Win32 clipboard calls, but microsoft/terminal#16715 is still in the Backlog milestone and the change is not in any shipped release — I updated to current stable 1.24.11911.0 specifically to test this, and the OLE formats are unchanged. So as long as Claude Code goes through OSC 52, users on RDP stay broken regardless of what they update.

Secondary, but it makes things measurably worse: stop re-emitting the same clipboard write on every re-render (#41954). Each write forces rdpclip into a fresh negotiation, and a second write arriving mid-negotiation is what turns a recoverable situation into "nothing arrives at all" (see section 3.2).

2. Workaround

Watches for the OLE formats and rewrites the clipboard as plain text. Must run under PowerShell 7 — Windows PowerShell 5.1's Set-Clipboard uses Clipboard.SetDataObject internally and re-creates the very OLE formats it is supposed to remove.

Four constraints, each hit the hard way — worth knowing before anyone writes their own version:

  1. Trigger on the presence of OLE formats, not on a text change. The TUI re-writes the same text repeatedly (#41954), so a text comparison sees "unchanged", skips, and leaves the re-poisoned state in place.
  2. Do not poll with Get-Clipboard. That holds the clipboard open continuously and blocks rdpclip from retrieving it — with polling, the client paste fails even though the resulting formats are clean. GetClipboardSequenceNumber() detects changes without opening the clipboard.
  3. Debounce ~1.5 s, then write a second time after another ~1.5 s (reason in section 3.2).
  4. Match formats by name, not by ID — 49161/49171 are assigned dynamically per session.

Practical cost: wait ~3 s after copying before pasting on the client.

# clipflush.ps1 - run with: pwsh -NoProfile -WindowStyle Hidden -File C:\Tools\clipflush.ps1
[CmdletBinding()]
param(
    [int]$IntervalMs     = 150,
    [int]$DebounceMs     = 1500,
    [int]$ConfirmDelayMs = 1500,
    [string]$LogPath     = "$env:LOCALAPPDATA\clipflush.log"
)

if ($PSVersionTable.PSEdition -ne 'Core') {
    throw 'Must run under PowerShell 7 - Windows PowerShell 5.1 creates the OLE formats itself.'
}

function Write-Log([string]$Message) {
    if ($LogPath) { Add-Content -LiteralPath $LogPath -Value ((Get-Date).ToString('HH:mm:ss.fff') + '  ' + $Message) }
}

Add-Type @'
using System;
using System.Text;
using System.Collections.Generic;
using System.Runtime.InteropServices;
public static class Clip {
    [DllImport("user32.dll")] public static extern uint GetClipboardSequenceNumber();
    [DllImport("user32.dll")] static extern bool OpenClipboard(IntPtr hWnd);
    [DllImport("user32.dll")] static extern bool CloseClipboard();
    [DllImport("user32.dll")] static extern uint EnumClipboardFormats(uint format);
    [DllImport("user32.dll")] static extern int GetClipboardFormatName(uint format, StringBuilder name, int max);

    public static List<string> NamedFormats() {
        var result = new List<string>();
        if (!OpenClipboard(IntPtr.Zero)) return result;   // locked right now: skip this round
        try {
            uint fmt = 0;
            while ((fmt = EnumClipboardFormats(fmt)) != 0) {
                var sb = new StringBuilder(256);
                if (GetClipboardFormatName(fmt, sb, sb.Capacity) > 0) result.Add(sb.ToString());
            }
        } finally { CloseClipboard(); }
        return result;
    }
}
'@

$oleFormats  = @('DataObject', 'Ole Private Data')
$lastSeq     = [Clip]::GetClipboardSequenceNumber()
$changedTick = 0
$pending     = $false

Write-Log "--- start (PID $PID, pwsh $($PSVersionTable.PSVersion)) ---"

while ($true) {
    $seq = [Clip]::GetClipboardSequenceNumber()
    if ($seq -ne $lastSeq) {
        $lastSeq = $seq; $changedTick = [Environment]::TickCount; $pending = $true
    }

    if ($pending -and (([Environment]::TickCount - $changedTick) -ge $DebounceMs)) {
        $pending = $false
        $ole = @([Clip]::NamedFormats() | Where-Object { $_ -in $oleFormats })

        if ($ole.Count -gt 0) {
            try { $text = Get-Clipboard -Raw -ErrorAction Stop } catch { $text = $null }

            # Text only. Running an image through Set-Clipboard would destroy it.
            if ($text) {
                try {
                    Set-Clipboard -Value $text -ErrorAction Stop
                    Start-Sleep -Milliseconds $ConfirmDelayMs
                    Set-Clipboard -Value $text -ErrorAction Stop   # isolated second change
                    $lastSeq = [Clip]::GetClipboardSequenceNumber()
                    Write-Log ("flushed {0} chars ({1})" -f $text.Length, ($ole -join '+'))
                } catch {
                    Write-Log "Set-Clipboard failed: $($_.Exception.Message)"
                }
            }
        }
    }

    Start-Sleep -Milliseconds $IntervalMs
}

3. Measurements

3.1 Clipboard formats

Enumerated with OpenClipboard(NULL) + EnumClipboardFormats + GetClipboardFormatName immediately after each copy.

After a Claude Code TUI copy (does NOT reach the RDP client):

Clipboard owner: NULL (data was flushed)
  49161  DataObject                Size=8
  13     CF_UNICODETEXT            Size=442
  49171  Ole Private Data          Size=168
  16     CF_LOCALE                 Size=4
  1      CF_TEXT                   Size=221
  7      CF_OEMTEXT                Size=221

After Set-Clipboard in PowerShell 7, same text (DOES reach the RDP client):

Clipboard owner: NULL (data was flushed)
  13     CF_UNICODETEXT            Size=442
  16     CF_LOCALE                 Size=4
  1      CF_TEXT                   Size=221
  7      CF_OEMTEXT                Size=221

This differs from the fingerprint in #67576 (conhost path, CF_UNICODETEXT only, no CF_LOCALE/CF_OEMTEXT). Here all four text formats are present and correctly rendered — no delayed rendering, no live owner. The only difference is the two OLE container formats, consistent with a WinRT DataPackage write going through OLE.

Relevant background: the RDP clipboard service excludes OLE formats by design and only transfers IDataObject-rendered formats of type TYMED_HGLOBAL — MS docs. That documents the filtering; it does not explain why the plain text alongside it is dropped too.

3.2 Timing matters

This took the longest to isolate, so it may save someone else the detour:

  • Rewriting the clipboard as plain text once, in isolation, reliably reaches the client.
  • Rewriting it as plain text immediately after the TUI copy (~250–400 ms later) does not — even though the resulting format list is verifiably identical to the working case above.

Interpretation: rdpclip appears to process the first (OLE) clipboard change, fail on it, and coalesce or drop the second change arriving while it is still negotiating. Waiting ~1.5 s before rewriting, then rewriting once more after another ~1.5 s, works reliably. This is inferred from behaviour, not from rdpclip internals.

3.3 Ruled out by testing

  • Restarting rdpclip.exe — no lasting effect.
  • "copyFormatting": "none" in Windows Terminal — affects HTML/RTF only, not the OLE containers.
  • A clipboard manager (Ditto) — it reads the clipboard but does not rewrite it, so nothing changes.
  • Updating Windows Terminal to current stable 1.24.11911.0 — OLE formats unchanged (see section 1).
  • The OpenConsole vs delegated conhost distinction from #67576 — already on the "good" side of it and still affected, so this is a separate failure mode.
ghabre · 20 days ago

same issue here.. i workaround by holding shift before dragging by mouse to copy, but this prevented me from using other tools like herdr cause it would copy left pane with queries

Showing cached comments. Read the full discussion on GitHub ↗