[BUG] Links open twice in Windows Terminal: Claude Code opens OSC 8 regions the terminal already activates
Summary
In Windows Terminal, ctrl+clicking any link in Claude Code's TUI opens it twice. The cause is
two independent openers acting on one click — not a duplicated click event. Windows Terminal
activates the OSC 8 hyperlink itself, and Claude Code also opens it from the forwarded mouse
report. Both are gated on ctrl.
Environment
- Claude Code 2.1.220
- Windows Terminal 1.24.11911.0 (Store package
Microsoft.WindowsTerminal_8wekyb3d8bbwe, only install on the machine) - PowerShell 7.6.3 (
C:\Program Files\PowerShell\7\pwsh.exe) - Windows 11 Home 10.0.26200
- Process chain:
claude.exe→pwsh.exe→WindowsTerminal.exe
Isolation
| # | Test | Tabs | Conclusion |
|---|---|---|---|
| 1 | Start-Process "https://example.com" from pwsh | 1 | Chrome / default-browser association is clean |
| 2 | echo https://example.com at bare pwsh prompt, ctrl+click | 1 | WT's URL auto-detection fires once |
| 3 | OSC 8 hyperlink emitted at bare pwsh prompt, ctrl+click | 1 | WT's OSC 8 handler fires exactly once |
| 4 | Link in Claude Code TUI, plain left-click | 0 | Claude Code requires the ctrl modifier |
| 5 | Link in Claude Code TUI, ctrl+click | 2 | two openers |
| 6 | Same as 5 with "experimental.detectHyperlinks": false | 2 | it is the OSC 8 path, not auto-detection |
| 7 | Link in Claude Code TUI, ctrl+shift+click | 1 | shift bypasses mouse reporting → Claude Code never sees the click → WT opens alone |
Test 3 establishes Windows Terminal opens OSC 8 links once on its own. Test 7 removes Claude
Code from the path and yields one tab. Together they place the second open inside Claude Code.
Reproduction for test 3:
$ESC = [char]0x1B
$osc8 = "{0}]8;;https://example.com{0}\OSC8 test link{0}]8;;{0}\" -f $ESC
Write-Host $osc8
Why this is not #68568 / warpdotdev/warp#13512
Those are the duplicated mouse-event class: the terminal delivers one physical click as two
click events and Claude Code acts on both. The published mitigation is de-duplicating events
within a time+coordinate window.
That mitigation would not fix this. Here exactly one click event arrives. The duplicate
opener is the terminal's own correct OSC 8 activation. Corroborating: click-to-expand on tool
results does not double in Windows Terminal, and plain left-click (test 4) produces zero opens
rather than two.
Expected
When Claude Code emits a region as an OSC 8 hyperlink, the terminal owns activation of that
region. Claude Code should not also open it.
Suggested fix
Either suppress Claude Code's own click-to-open over regions it wrapped in OSC 8, or add a
setting to disable click-to-open entirely — the latter was already requested in #18717 with no
flag available today.
Workaround
ctrl+shift+click opens exactly one tab.
Showing cached comments. Read the full discussion on GitHub ↗
3 Comments
Confirming this on Windows Terminal 1.24.11911.0 / Claude Code 2.1.220 / pwsh 7 / Win 11 26200, and I think I can name the exact mechanism — it's a press/release asymmetry in Windows Terminal that no report here has cited yet.
It is not a double dispatch. It's two different openers firing on two different phases of one click.
All line numbers below are
src/cascadia/TerminalControl/ControlInteractivity.cpponrelease-1.24.Phase 1 — mouse-down, Windows Terminal opens the link.
PointerPressed(:252) checks for a hyperlink under the cursor first, deliberately:Because the VT-forward sits in the
else if, the press is swallowed and never reaches the TUI. That part is working as designed.Phase 2 — mouse-up, Claude Code opens it again.
PointerReleased(:470) has no hyperlink guard at all:So the ctrl+mouse-up SGR report is forwarded with the Ctrl modifier bit intact. In the 2.1.220 bundle the guard on that path reads:
t.button & 24is the Alt|Ctrl mask, so a ctrl+mouse-up matches. 500 ms later it calls its own opener (rundll32 url,OpenURL) → tab 2.The only terminals excluded here are VS Code (
TERM_PROGRAM === "vscode") and whateverdA()covers. Windows Terminal is excluded by neither, so under"tui": "fullscreen"(full mouse tracking,?1000h ?1002h ?1003h ?1006h) the release always gets through.Net effect: the app receives a button-release for a button-press it never received, and treats it as a complete click.
---
Workaround that works today:
Ctrl+Shift+Click._canSendVTMouseInput(:681) short-circuits on Shift before consultingIsVtMouseModeEnabled():The release is therefore never forwarded, while the press-side hyperlink branch doesn't test Shift and still opens the link. Exactly one tab.
CLAUDE_CODE_DISABLE_MOUSE_CLICKS=1insettings.jsonalso fixes it, at the cost of in-TUI click targets and drag-select.experimental.detectURLs: falsedoes not help, and can't. It only gates Windows Terminal's regex pattern tree (_getPatterns()early-returns{}when unset).Terminal::GetHyperlinkAtBufferPositionchecksattr.IsHyperlink()for an explicit OSC 8 region first and unconditionally, never consulting the flag. Since Claude Code emits real OSC 8 hyperlinks, that setting was never in this code path. Worth stating explicitly because several reports here recommend it as a fix.Isolation check, in case it helps triage — emitting a raw OSC 8 hyperlink at a bare pwsh prompt in the same Windows Terminal profile opens one tab. The same link inside Claude Code opens two. So Windows Terminal's opener is firing exactly once; the second open is Claude Code's.
---
Suggested fix, in preference order:
MouseDownfor a button before treating itsMouseUpas a click. This is terminal-agnostic and fixes the whole class of "host swallowed the press" cases rather than special-casing one emulator.$WT_SESSION) to the existing bypass list alongside VS Code and Ghostty. Narrower, but a one-liner.Happy to test a build against this repro.
I can confirm this and have some measurements that may help narrow it down. I ran a local Node HTTP server that logs every incoming request with a millisecond timestamp, then clicked links pointing at it.
Environment: Windows 11 Home (26100), Windows Terminal 1.24.11911.0, Claude Code CLI running directly in Windows Terminal (no WSL), Chrome as default browser.
Result: A single Ctrl+click on a link rendered by Claude Code produces exactly 2 GET requests, consistently ~650 ms apart (measured gaps: 646 ms, 647 ms, 654 ms across three clicks). This happens for all three link styles: a markdown link with plain text, a markdown link whose visible text is the URL itself, and a bare URL.
Control tests (same machine, same session):
Start-Process <url>from PowerShell → exactly 1 request, so the OS-level http association and browser are clean.So the duplication only occurs when Claude Code and Windows Terminal both handle the same Ctrl+click: the terminal activates the OSC 8 hyperlink and Claude Code additionally opens the URL from the forwarded mouse event. The fact that a plain click does nothing suggests Claude Code's own handler is also gated on the Ctrl modifier, which is why both openers always fire together and there is no click-style workaround.
Independent corroboration, same platform:
$WT_SESSIONbeing set), PowerShell 7, Windows 11One additional data point that isn't explicitly called out above but is consistent with @shahaanf's root-cause analysis: the bug only reproduces under
"tui": "fullscreen". Toggling to/tui defaultin the same session, same window, same link, eliminates it entirely — exactly one tab, every time. That matches the theory that full VT mouse tracking (?1000h ?1002h ?1003h ?1006h) is what's enabled in fullscreen mode, which is the precondition for the mouse-up release event to get forwarded to Claude Code in the first place per the press/release asymmetry described above.Didn't file a new issue since this one already has the mechanism nailed down with source references from both sides (Windows Terminal's
ControlInteractivity.cppand Claude Code's bundledpendingHyperlinkTimerlogic) plus independent timing confirmation. Flagging mainly to add a data point on the fullscreen/default boundary, and to note it's still open on a fairly recent build.