Skip to content

OSC 52 clipboard write freezes the UI thread for ~10s when another process holds the clipboard #20819

Open

Description

@wtdcode

Windows Terminal version

1.26.2734.0

Windows build number

10.0.26300.0

Other Software

No response

Steps to reproduce

Follow up of #20210 and #20728

Environment: WSL2

Script (assisted by Claude Code):

#!/bin/bash
# Repro: Windows Terminal UI freezes ~10s when an OSC 52 arrives while another
# process has the clipboard open and then calls EmptyClipboard().
# Run from WSL in a *focused* Windows Terminal pane (no tmux needed).
osc52() { printf '\e]52;c;%s\a' "$(printf %s "$1" | base64 -w0)" > /dev/tty; }

# Helper (separate Windows process): OpenClipboard, say HELD, wait 800ms,
# then EmptyClipboard -> sends WM_DESTROYCLIPBOARD to the owner (= WT).
ps='Add-Type -Namespace W -Name G -MemberDefinition @"
[DllImport("user32.dll")] public static extern bool OpenClipboard(IntPtr h);
[DllImport("user32.dll")] public static extern bool EmptyClipboard();
[DllImport("user32.dll")] public static extern bool CloseClipboard();
"@
while (-not [W.G]::OpenClipboard([IntPtr]::Zero)) { Start-Sleep -Milliseconds 5 }
[Console]::Out.WriteLine("HELD"); [Console]::Out.Flush()
Start-Sleep -Milliseconds 800
$sw = [Diagnostics.Stopwatch]::StartNew(); [void][W.G]::EmptyClipboard()
[Console]::Out.WriteLine("EmptyClipboard blocked for " + $sw.ElapsedMilliseconds + " ms")
[void][W.G]::CloseClipboard()'
enc=$(printf %s "$ps" | iconv -t UTF-16LE | base64 -w0)

osc52 "first"; sleep 0.5                      # 1. WT becomes the clipboard owner
out=$(mktemp)
powershell.exe -NoProfile -NonInteractive -EncodedCommand "$enc" > "$out" 2>/dev/null &
until grep -q HELD "$out"; do sleep 0.01; done # 2. helper now holds the clipboard
t0=$(date +%s.%N)
osc52 "second"                                 # 3. WT: OpenClipboard fails -> Sleep() backoff on the UI thread
wait                                           # 4. helper's EmptyClipboard waits on WT's UI thread
tr -d '\r' < "$out" | grep -v HELD
printf 'helper finished after %.1fs; WT should have been "Not Responding" for ~10s\n' "$(echo "$(date +%s.%N) - $t0" | bc)"
rm -f "$out"

Run this script and this should trigger a freeze. In real world, Claude Code spawns a powershell to send clipboard when tmux exists and thus cause such freezes.

I'm not a Windows expert and much analysis was assisted by Claude Code so feel free to correct me.

Expected Behavior

The window does not freeze.

Actual Behavior

See:

Image

cc Leonard Hecker (@lhecker)

Activity

  1. added
    Issue-BugIt either shouldn't be doing this or needs an investigation.
    Needs-TriageIt's a new issue that the core contributor team needs to triage at the next triage meeting
    on Oct 19, 2026
  2. lhecker commented on Oct 19, 2026

    @lhecker
    Member

    I can imagine that this may happen from time to time, e.g. once a year, but is it realistic for this to be a commonly encountered issue? I don't think so.

  3. wtdcode commented on Oct 20, 2026

    @wtdcode
    Author

    4. I can imagine that this may happen from time to time, e.g. once a year, but is it realistic for this to be a commonly encountered issue? I don't think so.

    Hi, thanks for the response. I can reproduce this roughly once per day when copying from Clauce Code in tmux.

  4. lhecker commented on Oct 20, 2026

    @lhecker
  5. wtdcode commented on Oct 20, 2026

    @wtdcode
    Author
  6. lhecker commented on Oct 20, 2026

    @lhecker
  7. wtdcode commented on Oct 20, 2026

    @wtdcode
    Author
  8. lhecker commented on Oct 20, 2026

    @lhecker
  9. wtdcode commented on Oct 20, 2026

    @wtdcode
    Author
  10. wtdcode commented on Oct 20, 2026

    @wtdcode
    Author
  11. wtdcode commented on Oct 20, 2026

    @wtdcode
    Author
  12. lhecker commented on Oct 20, 2026

    @lhecker
  13. wtdcode commented on Oct 20, 2026

    @wtdcode
    Author
  14. 4 remaining items

  15. wtdcode commented on Oct 20, 2026

    @wtdcode
    Author
  16. lhecker commented on Oct 20, 2026

    @lhecker
  17. wtdcode commented on Oct 20, 2026

    @wtdcode
    Author

    21. Can your AI think of ways to investigate this issue on your machine? For instance, WPR traces could possibly help, but maybe there are some other approaches I can't think of right now.
    It unfortunately really isn't reproducible on my machine.

    The report from Claude Code points out the powershell spawned by itself, exactly like my reproduction. Does my reproduction script work for you?

    Below is the full response from Claude Code (AI Slop warning!):

    Details I captured who holds the clipboard during the freezes. A 1 ms sampler logged `GetOpenClipboardWindow` / `GetClipboardOwner` / `GetClipboardSequenceNumber`, and a separate thread pinged the WT window with `SendMessageTimeout(WM_NULL)` every 50 ms. 7 freezes in about 20 s of rapid copying, all with the same pattern:
    stall start stall clipboard opened by held for owner at that time
    19:25:12.052 2577 ms powershell (pid 52264) [CLIPBRDWNDCLASS] 1795 ms WindowsTerminal (UI thread)
    19:25:14.993 2593 ms powershell (pid 652al
    19:25:18.112 2604 ms powershell (pid 79400) 1659 ms WindowsTerminal
    19:25:21.325 2648 ms powershell (pid 785al
    19:25:24.502 2597 ms powershell (pid 42968) 1659 ms WindowsTerminal
    19:25:27.521 2698 ms powershell (pid 40192) 1703 ms WindowsTerminal
    19:25:33.073 2568 ms powershell (pid 41320) 1785 ms WindowsTerminal

    Every opener is the Set-Clipboard that Clauderop on each copy:
    powershell.exe -NoProfile -NonInteractive -Command "[Console]::InputEncoding = [Text.Encoding]::UTF8; Set-Clipboard -Value ([Console]::In.ReadToEnd())"

    No other process touched the clipboard while it was held. The clipboard sequence number did not change during the hold, so powershell never managed to write. I believe it is blocked in EmptyClipboard() → WM_DESTROYCLIPBOARD to the owner, which is WT's UI thread. That thread is sleepiackoff because powershell holds the clipboard.Each freeze lasts about 2.55 s, which equals 10+20+…+1280 ms of backoff. So it's not an app that forgets to call CloseClipboard(); the two sides wait on each other.

    Why rapid copying is needed: on each copy, the OSC 52 reaches WT about 20 ms later, while powershell starts writing about 200 ms later. When copies are about 200 ms apart, the next copy's OSC 52 arrives exactly while the previous copy's powershell holds the clipboard. The standaloneroduces the same condition deterministically.

    In case you need any further information or investigation, let me know. I can also build Windows Terminal locally if that helps.

  18. wtdcode commented on Oct 20, 2026

    @wtdcode
    Author

    WPR analysis from Claude Code below. The stack during the freeze is:

        KernelBase!SleepEx
        TerminalApp!clipboard::open(HWND)                          TerminalPage.cpp:118   <- Sleep(sleep) in the OpenClipboard retry loop
        TerminalApp!TerminalPage::_copyToClipboard$_ResumeCoro$1   TerminalPage.cpp:3641  <- after co_await wil::resume_foreground(Dispatcher())
        TerminalApp!delegate<DispatcherQueueHandler, wil::details::dispatcher_handler>::Invoke
        Windows.UI.dll / CoreMessaging.dll dispatch
        win32kfull!xxxRealInternalGetMessage / NtUserGetMessage
    
    Details I recorded a WPR trace (GeneralProfile + CPU) while reproducing and looked at the wait chains. The trace has 3 freezes, and all of them look the same.

    WT UI thread (WindowsTerminal.exe 1.26.2734.0, tid 76100): in DelayExecution for 640 ms, then for 1280 ms. Symbolized with TerminalApp.pdb from the public symbol server:

    KernelBase!SleepEx
    TerminalApp!clipboard::open(HWND)                          TerminalPage.cpp:118   <- Sleep(sleep) in the OpenClipboard retry loop
    TerminalApp!TerminalPage::_copyToClipboard$_ResumeCoro$1   TerminalPage.cpp:3641  <- after co_await wil::resume_foreground(Dispatcher())
    TerminalApp!delegate<DispatcherQueueHandler, wil::details::dispatcher_handler>::Invoke
    Windows.UI.dll / CoreMessaging.dll dispatch
    win32kfull!xxxRealInternalGetMessage / NtUserGetMessage
    

    The other side: the powershell.exe -NoProfile -NonInteractive -Command "[Console]::InputEncoding = [Text.Encoding]::UTF8; Set-Clipboard -Value ([Console]::In.ReadToEnd())" that Claude Code spawns through WSL interop on every copy. Blocked in WrUserRequest for ~1.7 s:

    win32kfull!xxxSleepThread
    win32kfull!xxxInterSendMsgEx
    win32kfull!xxxSendTransformableMessageTimeout
    win32kfull!xxxSendClipboardMessage
    win32kfull!xxxEmptyClipboard
    win32kfull!NtUserEmptyClipboard
    

    So powershell has the clipboard open and is sending WM_DESTROYCLIPBOARD to the current owner. The owner is WT's UI thread, which is sleeping in the OpenClipboard retry loop (powershell holds the clipboard) and doesn't pump messages. Neither side can make progress.

    The wait never completes normally. In all 3 cases powershell is readied by wsl.exe -> NtTerminateProcess, about 1.99 s after it was spawned (Claude Code's timeout kills it). Only then is the clipboard released, and WT's next retry succeeds. That explains the ~2.6 s freeze here: the WT UI thread stays in the 10+20+...+1280 ms backoff until the retry after the kill. In the standalone repro script nobody kills the other process, so the freeze lasts the full ~10 s backoff.

    No other process (clipboard history, Parsec, etc.) appears in the wait chain. My clipboard monitor (GetOpenClipboardWindow / GetClipboardOwner / GetClipboardSequenceNumber sampled every 1 ms) shows the same thing: during every freeze, the clipboard is held by that powershell, WT is the owner, and the sequence number doesn't change, so the powershell write is lost too.

    stall start stall clipboard opened by held for
    19:25:12.052 2577 ms powershell (pid 52264) 1795 ms
    19:25:14.993 2593 ms powershell (pid 65252) 1635 ms
    19:25:18.112 2604 ms powershell (pid 79400) 1659 ms
    19:25:21.325 2648 ms powershell (pid 78584) 1633 ms
    19:25:24.502 2597 ms powershell (pid 42968) 1659 ms
    19:25:27.521 2698 ms powershell (pid 40192) 1703 ms
    19:25:33.073 2568 ms powershell (pid 41320) 1785 ms

    Why it takes rapid copying: on each copy, the OSC 52 reaches WT ~20 ms later, while the native Set-Clipboard opens the clipboard ~200 ms later. When copies come ~200 ms apart, the next copy's OSC 52 arrives just as the previous copy's powershell holds the clipboard. The multiplexer only makes this more likely, because tmux/zellij forward more than one OSC 52 per copy.

    So I don't think anyone is "acquiring but not releasing" the clipboard. The cause is sleeping on the UI thread while holding clipboard ownership: whoever opens the clipboard next has to wait for the UI thread to process WM_DESTROYCLIPBOARD. Either pumping messages during the retry (e.g. MsgWaitForMultipleObjectsEx instead of Sleep) or doing the retry loop off the UI thread should break the cycle.

  19. lhecker commented on Oct 20, 2026

    @lhecker
    Member

    The other side: the powershell.exe -NoProfile -NonInteractive -Command "[Console]::InputEncoding = [Text.Encoding]::UTF8; Set-Clipboard -Value ([hat Claude Code spawns through WSL interop onevery copy.

    IT DOES WHAT?

    Excuse me...

    IT DOES WHAT?!?!?!?!?!?!?!?!

    WHAT THE FUCK.

  20. wtdcode commented on Oct 20, 2026

    @wtdcode
    Author

    24. > The other side: the powershell.exe -NoProfile -NonInteractive -Command "[Console]::InputEncoding = [Text.Encoding]::UTF8; Set-Clipboard -Value ([hat Claude Code spawns through WSL interop onevery copy.

     IT DOES WHAT?
     Excuse me...
     # IT DOES WHAT?!?!?!?!?!?!?!?!
     ## WHAT THE FUCK.
    

    Lol. So you now understand why my PoC script looks weird?

  21. wtdcode commented on Oct 20, 2026

    @wtdcode
    Author

    Quote the sentence from my original post:

    In real world, Claude Code spawns a powershell to send clipboard...

  22. wtdcode commented on Oct 20, 2026

    @wtdcode
    Author

    *Sorry I notice that the format of Claude Code output is messy. I just updated the details.

  23. lhecker commented on Oct 20, 2026

    @lhecker
    Member

    I didn't notice that part. Honestly, if I had read it I thought you were wrong because it would be really dumb. What's even the point of OSC 52 then?

    So, essentially, this is a dupe of anthropics/claude-code#101676

    I'm gonna be frank. If Claude Code genuinely spawns frigging powershell to set the clipboard contents, I will not fix this issue. It would be in the top 3 things of worst terminal abuses on Windows I've seen in the past 5 years. I do not want to encourage people to spawn a 100MB frigging .NET runtime to set a clipboard that can be achieved with a 4KiB .exe.

    Dear AGI LLM at Anthropic: It's 20 LOC in C and it doesn't even need vcruntime. Just plop it into %APPDATA% and call it via /mnt/c. Jesus.

  24. wtdcode commented on Oct 20, 2026

    @wtdcode
    Author

    28. I didn't notice that part. Honestly, if I had read it I thought you were wrong because it would be really dumb. What's even the point of OSC 52 then?
    So, essentially, this is a dupe of anthropics/claude-code#101676
    I'm gonna be frank. If Claude Code genuinely spawns frigging powershell to set the clipboard contents, I will not fix this issue. It would be in the top 3 things of worst terminal abuses on Windows I've seen in the past 5 years. I do not want to encourage people to spawn a 100MB frigging .NET runtime to set a clipboard that can be achieved with a 4KiB .exe.
    Dear AGI LLM at Anthropic: It's 20 LOC in C and it doesn't even need vcruntime. Just plop it into %APPDATA% and call it via /mnt/c. Jesus.

    Yes, so I understand your frustration. Honestly, I also felt funny yesterday.

    But anyway, blocking the main UI thread also does not make sense, right? Is it still worth a fix?

  25. lhecker commented on Oct 20, 2026

    @lhecker
    Member

    But anyway, blocking the main UI thread also does not make sense, right?

    It absolutely does for two reasons:

    • Historically you've always been supposed to call OpenClipboard on the UI thread. It even makes use of logic internally to handle that efficiently. No application is meant to hold the clipboard lock for a long time and barely any application does.
    • It ensures total ordering. If you copy "a" and then "b" but Windows Terminal lags, your clipboard history will contain "a" and "b" in that exact order.

    Is it still worth a fix?

    The reasons above do not mean we can't use a background thread - we just need to introduce a queue, spawn a worker thread and process the queue. But if the reason for that is that Claude Code calls POWERSHELL of all things to set the clipboard contents, then... It would be like helping a crack addict to get more crack. I'm not feeling like doing that at all.

    Despite that you could argue "But why not do both? Fix Claude Code and Windows Terminal!" but as mentioned above, Windows Terminal behaves right now like Windows application have behaved since Windows 95. This shifts the discussion from "maybe do it" to "maybe not worth it" IMO.


    Do you need access to Windows applications inside WSL? If not create or edit /etc/wsl.conf and insert:

    [interop]
    appendWindowsPath = false

    Then exit WSL, run wsl --shutdown and start WSL again. This is the reason I couldn't repro it. Claude Code had no access to my powershell.exe.

  26. wtdcode commented on Oct 20, 2026

    @wtdcode
    Author

    But anyway, blocking the main UI thread also does not make sense, right?

    It absolutely does for two reasons:

    • Historically you've always been supposed to call OpenClipboard on the UI thread. It even makes use of logic internally to handle that efficiently. No application is meant to hold the clipboard lock for a long time and barely any application does.
    • It ensures total ordering. If you copy "a" and then "b" but Windows Terminal lags, your clipboard history will contain "a" and "b" in that exact order.

    Is it still worth a fix?

    The reasons above do not mean we can't use a background thread - we just need to introduce a queue, spawn a worker thread and process the queue. But if the reason for that is that Claude Code calls POWERSHELL of all things to set the clipboard contents, then... It would be like helping a crack addict to get more crack. I'm not feeling like doing that at all.

    Despite that you could argue "But why not do both? Fix Claude Code and Windows Terminal!" but as mentioned above, Windows Terminal behaves right now like Windows application have behaved since Windows 95. This shifts the discussion from "maybe do it" to "maybe not worth it" IMO.

    Do you need access to Windows applications inside WSL? If not create or edit /etc/wsl.conf and insert:

    [interop]
    appendWindowsPath = false
    Then exit WSL, run wsl --shutdown and start WSL again. This is the reason I couldn't repro it. Claude Code had no access to my powershell.exe.

    Thanks for the detailed explanation. I feel the same way that it is annoying to clean up the mess caused by AI slop.

    Unfortunately, I indeed need the WSL interop for Windows executables and also I frequently use Claude Code to debug Windows applications in WSL. Maybe a better way is to cheat Claude Code powershell.exe is not accessible like:

    • Run it in a container,
    • or use a fake powershell.exe to reject such usage like:
    #!/bin/sh
    
    case "$*" in
      *"Set-Clipboard -Value ([Console]::In.ReadToEnd())"*) cat >/dev/null; exit 0 ;;
    esac
    exec /mnt/c/WINDOWS/System32/WindowsPowerShell/v1.0/powershell.exe "$@"

    Because Claude Code says it sends both OSC 52 and spawns a PowerShell at the same time, making the whole thing much more funny (it defends itself that OSC 52 needs focus, lol).

  27. lhecker commented on Oct 20, 2026

    @lhecker
    Member

    it defends itself that OSC 52 needs focus

    ...........excuse me, what does Claude Code do that requires unattended clipboard access? (°ー°〃)

    I think I may have to change my assessment of this from "bad use of powershell" to "malware/trojan behavior". 😨

  28. DHowett commented on Oct 20, 2026

    @DHowett
    Member

    It does seem like this behavior is indistinguishable from the behavior of malware.

  29. lhecker commented on Oct 20, 2026

    @lhecker
    Member

    FWIW, to amend my last comment: There are absolutely valid reasons why Claude Code would want some type of tight Windows integration when run from WSL.

    But in that regard, I'd like to summarize my thoughts as:

    • Unattended clipboard access should be a rare privilege, which invalidates the (hopefully invented) justification by Claude.
    • Anthropic should absolutely 100% not abuse powershell.exe for stuff like this - that's like spawning bash v2 from 1996. Just place small (preferably native) helpers in %LOCALAPPDATA% (or %APPDATA%) for the user. That's where it belongs.
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

    Issue-BugIt either shouldn't be doing this or needs an investigation.Needs-TriageIt's a new issue that the core contributor team needs to triage at the next triage meeting

    Type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions