Skip to content

feat(terminal): bundle Microsoft's ConPTY (conpty.dll + OpenConsole.exe) - #188

Merged
mortenaslo merged 2 commits into
mainfrom
feat/bundled-conpty
Nov 6, 2026
Merged

mortenaslo merged 2 commits into
mainfrom
feat/bundled-conpty

Conversation

@mortenaslo

Copy link
Copy Markdown
Contributor

Sessions now run on Microsoft's current ConPTY (Microsoft.Windows.Console.ConPTY 1.25.260930003, stable, Microsoft-signed, MIT) instead of the one built into Windows. Windows Terminal and VS Code do the same.

Why

Measured against conhost 10.0.26100:

  1. The built-in ConPTY drops OSC sequences written back to the program, for example colour replies and OSC 7501 replies.
  2. It answers the DA1 query itself, so any reply that has to come from the real terminal arrives too late.

The bundled ConPTY does neither: raw OSC input arrives intact, and DA1 is forwarded to xterm.js. Claude Code 2.1.319 logs programStatus=no (probe: no reply to OSC 7501 ; ?) on the built-in ConPTY and programStatus=yes (probe: OSC 7501 reply) on the bundled one. This unblocks OSC 7501 status reporting in #187, for any program that checks for support before reporting, not only Claude.

What changes

  • Packaging: both files ship as loose files beside the exe in the build output, the single-file publish, the MSI and the ZIP. They come from a PackageReference, not binaries committed to git, and the architecture is picked per build (x64 by default).

  • Loading: conpty.dll is loaded by full path only (AppContext.BaseDirectory, function pointers), never by bare name, which avoids DLL search-order hijacking. kernel32 is loaded from System32 by full path.

  • Fallback to the built-in ConPTY, logged to crash.log, when any of these hold:

    • UseBundledConPty is false
    • a file is missing
    • loading the DLL fails
    • creating a console fails 3 times in a row (a single failing terminal retries on the built-in ConPTY regardless)

    Resize and close always go to the ConPTY that created the console.

  • Early output is kept: output that arrives before the first subscriber is buffered and delivered in order. Sessions attach after Start, and the bundled ConPTY's first bytes include a DA1 query that xterm.js must answer.

  • Run commands stay on the built-in ConPTY. They are headless, and the bundled one leaves a program's own DA1 query unanswered there (measured). Their startup is unchanged: 45 ms vs 44 ms median.

  • Escape stripping: the three escape-stripping regexes now also strip ESC[1t / ESC[c, so they stay out of search.

  • CI and signing:

    • CI fails if the MSI or the publish output lacks either file, or if either file is not validly Microsoft-signed.
    • Exe signing now targets CodeShellManager.exe only. The old folder filter would have re-signed Microsoft's OpenConsole.exe with our certificate.

Review

A read-only review found one real race: the first PTY output, including the startup DA1 query, could be dropped before the bridge subscribed. It also flagged x64-only package paths and a single failure permanently switching the process to the built-in ConPTY. All three are fixed in the second commit. Loader security, the function-pointer signatures, the WiX layout, the signing input and the regex scope were checked and found sound.

Verified

  • 782 unit tests pass (17 new: loader/fallback, early-output ordering under concurrency, failure policy).
  • UI tests 10/10 on this build, with test(ui): make the FlaUI suite pass again, without the mouse or keyboard #184's test fixes applied.
  • Resize, exit detection, the job-object kill, throughput and hard-kill cleanup behave the same on both ConPTYs; no orphaned processes.
  • WSL, SSH, pwsh with PSReadLine, cmd and claude all launch.
  • MSI built locally and its contents checked.

Not verified

  • The release workflow itself: the files: signing input and the new guards. The first tagged build will exercise them.
  • ARM64 and x86 at runtime. The fallback covers them.
  • Day-to-day use: rendering in Claude, vim/htop-style full-screen apps, resize.

🤖 Generated with Claude Code

https://claude.ai/code/session_01NsYm9iLc2aRnRDfV2uZRQX

mortenaslo and others added 2 commits November 6, 2026 18:01
Sessions now run on the bundled ConPTY from Microsoft.Windows.Console.ConPTY
1.25.260930003 instead of the inbox conhost. Measured on conhost 10.0.26100:
the inbox ConPTY drops OSC sequences written to its input and answers DA1
itself, so terminal replies (OSC 7501, OSC 11, …) never reach programs in
time. Claude Code logs programStatus=no on inbox, =yes on the bundled one.

- csproj: PackageReference (ExcludeAssets=all) + explicit loose copies of
  conpty.dll and OpenConsole.exe beside the exe, ExcludeFromSingleFile.
- ConPtyApi: binds Create/Resize/ClosePseudoConsole by FULL PATH (bundled
  conpty.dll from AppContext.BaseDirectory, inbox from System32 kernel32);
  never a bare DLL name. Each PTY keeps the API that created its HPCON.
- Fallback to inbox, logged once: setting UseBundledConPty off, a file
  missing, load/export failure, or a failing bundled CreatePseudoConsole.
- Run commands stay on the inbox ConPTY: headless, and OpenConsole consumes
  the first DA1 answer for its own startup query (measured).
- ANSI strip regexes (OutputIndexer, RunInstance, AlertDetector) now also
  strip CSI finals c and t (OpenConsole opens with ESC[1t ESC[c).
- MSI installs both files; CI fails if the .wxs lacks them, if publish/
  lacks them, or if they are not validly Microsoft-signed. Exe signing now
  targets CodeShellManager.exe by name (no longer re-signs OpenConsole.exe).
- CLAUDE.md "Bundled ConPTY" section; README mention; ConPtyApiTests.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NsYm9iLc2aRnRDfV2uZRQX
…h files, softer demotion

- A1: PseudoTerminal.DataReceived is now backed by EarlyOutputBuffer. Sessions
  start the PTY before the bridge subscribes, so the reader's first chunks had
  no subscriber and were dropped — on the bundled ConPTY that is
  ESC[1t ESC[c ESC[?1004h ESC[?9001h, and losing ESC[c makes OpenConsole eat the
  program's DA1 answer later. Output before the first subscriber is held
  (capped) and flushed to it under the same lock the reader uses, so nothing
  is lost or reordered. Verified with a subscriber attached 800ms after Start.
- A2: the csproj picks conpty.dll/OpenConsole.exe for the target architecture
  (RuntimeIdentifier, then PlatformTarget, then the build machine's OS
  architecture; default x64). The bind test skips the load assertion when the
  DLL and the test process architectures differ.
- A3: a failing bundled CreatePseudoConsole still falls back for that terminal
  and is logged each time, but only 3 consecutive failures (reset on success)
  switch the rest of the run to inbox (BundledFailurePolicy).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NsYm9iLc2aRnRDfV2uZRQX
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant