Skip to content

Instantly share code, notes, and snippets.

@YoraiLevi
Last active August 2, 2026 13:17
Show Gist options
  • Select an option

  • Save YoraiLevi/acaa0b584389cecae0036db4624f0210 to your computer and use it in GitHub Desktop.

Select an option

Save YoraiLevi/acaa0b584389cecae0036db4624f0210 to your computer and use it in GitHub Desktop.
Persistent, programmatically drivable shell sessions on native Windows 11 — a working engineer's guide to terminal middle layers (ConPTY session daemons, PTY-as-a-service, agent-drivable control surfaces, and the no-PTY alternative). Includes the underlying research map and the provenance manifest of every source consulted.

Persistent, Programmatically Drivable Shell Sessions on Native Windows 11 — A Working Engineer's Guide

You have a coding-agent CLI that runs for hours. It is doing something long — a refactor across a worktree, a test loop, a migration — and right now the only thing keeping it alive is a terminal pane inside a desktop app. Close the app, restart it after an update, or swap it for a different one, and every running agent dies with it. That is not a hypothetical failure: agentic development environments churn fast.

A word on "the census", because this guide leans on it throughout. The census is the candidate pool behind this document: projects reached through GitHub topic search, GitHub code search and targeted follow-up, with every load-bearing claim checked against source code, release artifacts, or a project's own primary documentation. Its reach has a hard bound — anything not indexed by GitHub code search, including self-hosted forges and unreleased internal tools, is outside it by construction rather than absent from the world (see Coverage gaps). Superlatives in this guide ("the only identity-scoped namespace found anywhere") are scoped to that pool and mean nothing beyond it.

That census recorded six ADEs that died or closed in the eight months before it was written, and it can name all six: Crystal (deprecated February 2026, and it produced two descendants rather than one — Nimbalyst, the named successor, and Pane, which still carries a renamed docs/CRYSTAL_ARCHITECTURE.md), Terragon (shut down January/February 2026), Roo Code (archived, last push 2026-05-15, no stated reason), Vibe Kanban (sunset 2026-04-24 at 27,622 stars — its second-to-last commit is literally "Add README sunsetting banner"), DevPod (abandoned; its last release is v0.7.0-alpha.34 from 2025-06-23), and Daytona (closed-sourced June 2026, leaving 72k stars on a repository whose root now contains only a README and an assets folder). One method rule falls straight out of that list and is worth carrying: a last-push date more than sixty days old needs a specific check for a shutdown notice, not just a date — "stale" and "sunset" look identical in repository metadata, and this research conflated them once.

Concretely, the shape you are working against is a spawn chain. An ADE (Agentic Development Environment — a desktop app that spawns coding-agent CLIs, typically one per git worktree, each in its own terminal pane) spawns the agent today, and in doing so it calls CreatePseudoConsole and holds the resulting handle. In this case the ADE is ORCA (stablyai/orca, Electron plus node-pty). What you want is one more link in that chain: the ADE spawns your middle layer; your middle layer calls CreatePseudoConsole and holds the HPCON; the agent CLI runs as its child. The usual phrasing — "a layer under the shell and above the agent" — is spatially confusing, because the shell and the agent occupy the same slot and the middle layer is above the agent in the spawn tree, not below it. What matters is who holds the handle. The hook point for inserting that link already exists and is verified in source: ORCA's per-agent launch command is overridable through agentCmdOverrides, declared in src/shared/types.ts and consumed at src/shared/tui-agent-launch-command.ts:30, where the lookup is const override = args.cmdOverrides[args.agent]. You give it a different command; that command is your middle layer. A worked example of the override entry is in If you are wrapping under ORCA specifically.

And a note on "any ADE", because the swap is the entire point. An override hook like ORCA's is what makes a middle layer insertable at all, so it is worth knowing which other ADEs have one. Among native-Windows ADEs the census found four that document a genuine free-form "point me at any executable" hook a third party can use without forking source: ORCA (agentCmdOverrides); kandev (kdlbs/kandev, AGPL-3.0 — Settings → Agents → Add TUI Agent, the command string parsed with strings.Fields, documented in docs/public/agents-and-profiles.md); Pane (dcouple/Pane, AGPL-3.0 plus attribution — --tool-command <command>, documented in docs/RUNPANE_CLI_CONTRACT.md); and Zed (agent_servers in settings.json — but ACP-shaped, so the spawned process must speak ACP JSON-RPC rather than own a raw PTY, and that reading was never re-verified in any pass of this research). Three more document an override at least as good and are out on platform rather than on override: cmux (cmux.json, cmux hooks setup --agent; macOS only), superset (the best-documented override of any candidate, but its own README says "Windows/Linux untested"), and sculptor ("run and manage any terminal-based agents"; Mac, Linux and Linux-ARM64 downloads only). The conclusion to carry: override hooks are not rare; native-Windows override hooks are. Note also which project does not have one — Agent Orchestrator, whose 30 compiled Go packages under backend/internal/adapters/agent/ leave no way to point it at an arbitrary CLI. That matters because its ao pty-host subsystem is otherwise the closest architectural match to the layer described here.

Three constraints frame everything that follows, and they are pass/fail. Native Windows 11, no WSL — anything POSIX-only, WSL-required, or Linux-container-only is out. Open source — commercial tools appear here only as context. Headless-first — anything that needs a human attached, or a GUI, to be useful has failed the thing you are actually buying. A fourth consideration is a weighting rather than a filter: cross-platform is a plus, not a requirement, so Windows-only is a demerit and not a disqualifier. There is a fifth property that is not a constraint at all and would kill candidates: nestability. Your middle layer runs inside the ADE's own PTY, so prefix-key collisions, resize propagation, alt-screen handling, mouse passthrough and double VT parsing are live problems rather than academic ones. Be clear that it has killed nothing yet, though — double-ConPTY nesting has never been empirically tested for any candidate in this research, so it is a hazard with no measurements behind it. Every star count, release date and status in this guide is a snapshot taken 2026-08-02; those numbers churn weekly. One rule for re-checking them: read a repository's pushed_at, never its updated_at, because the latter ticks on stars and watches and flipped a staleness verdict once during this research.

Every tool in this space is an answer to four questions, and the four are independent of each other. Read them once; the rest of this guide points back at them by their handles.

  1. Ownerwho calls the PTY-spawn API. On Windows that is CreatePseudoConsole, and whoever calls it holds the resulting HPCON handle, which makes that process the session's owner and the only party in the world that can resize it.
  2. Lifetimewhether that owner outlives the client that started it. This guide grades persistence L0 to L3: L0 dies with the connection, L1 survives a client disconnecting, L2 survives the ADE being killed or upgraded, L3 survives a reboot.
  3. Rendezvouswhat address the client and the owner meet at. A named pipe, a loopback TCP port, an AF_UNIX socket, an HTTP endpoint. Note in passing that AF_UNIX has been a genuine Windows transport since Windows 10 1803, so a UnixListener in somebody's source proves nothing about platform support in either direction.
  4. Payloadwhat crosses that boundary. Raw child bytes, or a parsed screen model re-rendered back into ANSI — the answer that decides whether your terminal grid gets built twice on every keystroke.

Answer Owner, Lifetime, Rendezvous and Payload for any tool, including ones nobody has written yet, and you know what it is, what it costs you, and which of your problems it leaves untouched. Everything else — verb sets, config formats, star counts, whose Rust it is — hangs off those four answers.

When you need a session daemon, and when you do not

Reach for a PTY-owning session daemon when the agent process must survive the thing that launched it, when a human has to be able to walk up and type into a session that is already running, or when you need to know what is actually on the screen — because a TUI's rendered state is the only place some information exists. Reach for a no-PTY channel when you only need to start work, observe it, answer its questions and collect its results. That second case is more of the job than it first appears, and the tooling for it is better than the tooling for the first.

The strongest headless option owns no PTY at all. claude -p with --input-format stream-json --output-format stream-json is a full-duplex NDJSON channel over ordinary pipes: you write newline-delimited JSON to stdin, you read newline-delimited JSON from stdout, and there is no console, no ConPTY, no terminal emulator and therefore no nesting problem anywhere in the picture.

# Full-duplex NDJSON over ordinary pipes. Drive this from any language that can spawn a process.
claude -p `
  --input-format stream-json `
  --output-format stream-json `
  --verbose `
  --include-hook-events `
  --forward-subagent-text `
  --include-partial-messages `
  --replay-user-messages

The flags are documented without platform qualification in the Claude Code CLI reference, and --forward-subagent-text carries a parent_tool_use_id so you can reassemble the subagent tree. Be clear-eyed about two things. First, "behaves identically on Windows and Linux" is an inference, not a verified measurement: the flags are unqualified and the transport is ordinary pipes, but nobody has run this on both platforms and diffed the output. It is the load-bearing cross-platform claim of the whole no-PTY architecture and it is untested. Second, this is print/headless mode. It does not attach to an existing interactive TUI — it is a way to build a middle layer that spawns the agent with no PTY in the first place.

Hooks are the write path, and they can answer prompts with no human present. The hooks reference documents five hook types, not the three that get repeated in blog posts: command, prompt (an LLM evaluates the decision), agent (spawns an agentic verifier), http (POSTs the event JSON to a URL and reads the decision out of the response) and mcp_tool (calls a tool on an already-connected MCP server), governed by an allowedHttpHookUrls policy allowlist. PreToolUse returns a permissionDecision of allow/deny/ask/defer plus an updatedInput that replaces the tool's arguments, and the docs state the headless trick outright: "AskUserQuestion and ExitPlanMode require user interaction and normally block in non-interactive mode with the -p flag. Returning permissionDecision: "allow" together with updatedInput satisfies that requirement." The http type is the important one architecturally — your middle layer can BE the endpoint, a long-lived process you own, which sidesteps the fact that a stdio MCP server cannot hold persistent sessions because the client spawns and reaps it.

Three limits on that, all load-bearing and all easy to miss. (Citation note for all three: the hooks reference is a rendered HTML page with no stable line numbering, so the line numbers below are into a plain-text extraction of it and are given only as a locator. The quoted phrase is the checkable part — search the page for it.)

First, the hooks reference states — under the hook-type menu, at extracted line 549 and again at line 221 — that "SessionStart and Setup support command and mcp_tool hooks. They don't support http, prompt, or agent hooks", so the two bootstrap events a middle layer most wants to own are exactly where the HTTP-endpoint trick is unavailable. Thirteen events do support all five, including PreToolUse, PermissionRequest, Stop, SubagentStop and UserPromptSubmit, so the technique is intact where it counts.

Second — and this narrows the remedy for the first, so read them together — the same page states at extracted line 469 that "MCP tool hooks are available on every hook event once Claude Code has connected to your MCP servers. SessionStart and Setup typically fire before servers finish connecting, so hooks on those events should expect the 'not connected' error on first run." The obvious fallback for the bootstrap events is "use mcp_tool or command there", and half of that fallback is unreliable at exactly those two events. The only dependably available bootstrap hook type is command.

Third, at extracted line 328: "As of v2.1.199, an MCP tool whose server marks it with _meta["anthropic/requiresUserInteraction"] is stricter: a hook can't skip its approval prompt with 'allow', with or without updatedInput" — the headless-answering trick has an explicit opt-out that individual tool authors control, so a tool you depend on can decide to block you.

OpenTelemetry is the liveness channel, and it costs you nothing structurally — it does not touch the PTY, so it survives an ADE swap for free. Claude Code emits 34 named claude_code.* identifiers; note that this is a mix of metrics, events and spans, not 34 events, so do not quote it as an event count. The one that matters when no human is watching is claude_code.tool.blocked_on_user — "is the agent stuck waiting for someone".

$env:CLAUDE_CODE_ENABLE_TELEMETRY      = '1'   # the master switch, and it is NOT an OTEL_* var
$env:CLAUDE_CODE_ENHANCED_TELEMETRY_BETA = '1' # traces, not just metrics
$env:OTEL_LOG_TOOL_DETAILS             = '1'   # content is redacted by default; opt in per-field

Three separable things live in that block, and it is worth taking them one at a time.

The switch. CLAUDE_CODE_ENABLE_TELEMETRY=1 is the master switch and it is deliberately not an OTEL_* variable, which is the single most common way people fail to turn this on. Traces additionally require CLAUDE_CODE_ENHANCED_TELEMETRY_BETA=1; the claude_code.hook span on top of that requires ENABLE_BETA_TRACING_DETAILED=1, a BETA_TRACING_ENDPOINT, and org allowlisting. TRACEPARENT is inherited by both Bash and PowerShell subprocesses — but the hooks reference warns (extracted line 152) that "Claude Code removes OTEL_* exporter variables from every subprocess it spawns, including hooks," so your telemetry config does not reach grandchildren and you must re-establish it there yourself.

The redaction defaults. Content is redacted by default and you opt in field by field: OTEL_LOG_USER_PROMPTS, OTEL_LOG_TOOL_DETAILS, OTEL_LOG_TOOL_CONTENT, OTEL_LOG_ASSISTANT_RESPONSES and OTEL_LOG_RAW_API_BODIES — the last of which has a =file:<dir> mode that writes untruncated request and response bodies to disk behind a body_ref path. Turn that one on deliberately or not at all.

The transcript tree. Alongside telemetry there is a durable record: the JSONL transcripts under ~/.claude/projects/<project>/<session>.jsonl, which are a tree, not a file. Inspecting one on native Windows 11 (build 26200) shows a sibling <session-uuid>/ directory holding subagents/agent-<id>.jsonl plus agent-<id>.meta.json sidecars carrying {agentType, description, toolUseId, spawnDepth}, alongside tool-results/ and workflows/ — so the subagent fan-out topology is readable from the filesystem alone. These internals are undocumented and version-fragile; CLAUDE_CONFIG_DIR relocates the whole tree and cleanupPeriodDays garbage-collects it, so anything treating JSONL as durable needs its own retention policy.

So state plainly what PTY ownership buys that none of the above can — and be careful with the usual formulation, because the obvious one is false. It is tempting to say PTY ownership buys "the exact on-screen state". It does not: reconstructing on-screen state requires the byte stream plus a VT emulator, not a PTY. asciinema's relay does exactly that and never touches a PTY — its own streaming documentation states that "the server maintains comprehensive state for each active stream by running the whole stream through asciinema's own virtual terminal emulator." What PTY ownership buys is one step upstream: the byte stream itself — the bytes a full-screen TUI paints, which it paints only because something is presenting itself as a terminal to it, and which appear in no structured event anywhere. Given those bytes, anyone with an emulator can derive the screen. So the two things are: the byte stream of a program that insists on a terminal, and a human being able to type into a live session. If neither is on your requirements list, the no-PTY architecture dominates and most of this guide is optional reading — every hard Windows problem in it (the ConPTY startup handshake, nesting, prefix-key collisions, resize propagation, double VT parsing) simply does not arise, because you never own a PTY. If either one is on the list, no amount of structured-event plumbing substitutes, and you are shopping for a daemon.

Before you build anything, check whether Windows already does it. A disconnected RDP session keeps every process running with a live console, and you reattach by reconnecting; tscon moves a session between connections. That is persistence plus reattach, in the box, with no third-party code. It fails you on granularity — it is whole-session, so there is no per-worktree agent tab — and Microsoft's tscon documentation states "You can't connect to the console session" and "You must have Full Control access permission or Connect special access permission". (The often-repeated companion claim that client SKUs allow only one interactive session is not on that page and no separate source for it was recorded here; treat it as folklore worth checking against your own SKU before you plan around it.) But if your real requirement is "my long-running work should not die when I close the laptop lid," measure this before writing a line of Rust.

Do not plan around this being solved upstream. #24365 — "Expose ACP over network transport (claude serve) for remote…", an unambiguous network-attach request — was stale-bot-closed on 2026-03-14 and locked on 2026-03-21, with 3 total reactions and 3 of its 4 comments from bots. That one closure is what the conclusion rests on. A second issue, #6686, is often cited alongside it and carries less weight than it looks: it was closed not_planned on 2026-02-09 despite 551 reactions, but its title is "Feature Request: Add support for Agent Client Protocol (ACP)" — plain ACP support, not network attach — and Claude Code is an ACP implementer today, so that request was closed not_planned and the feature shipped anyway. Read the record as declined once, clearly, on the thing you care about, rather than as declined twice. Either way it is a weak bet that Claude Code grows a natively network-attachable session, which is precisely what strengthens the case for an external layer that does not depend on it.

How to read this document

The rest of this guide is organised by option, not by verdict. Each candidate gets the same treatment: what it is, how it works, the commands to install and drive it, and the specific way it fails in the real world. Read the framing section above and the playbook at the end; read the option sections that survive your own answers to Owner, Lifetime, Rendezvous and Payload. There is no ranking here, because the ranking depends on which of the four you cannot compromise on — and the playbook opens with a filter ladder that turns your answers into a shortlist mechanically.

One convention for every command block in this guide. Where the shell matters, the target is PowerShell 7+ (pwsh). Windows PowerShell 5.1 differs in two ways that bite here: Add-Type -MemberDefinition behaves the same but *> stream redirection and the default [Console]::OutputEncoding do not, and the latter can silently mangle UTF-8 on a pipe into a native executable. Where a block spells out powershell.exe, that is deliberate and the reason is stated in place.

Section What is in it
When you need a session daemon, and when you do not The no-PTY channel, what PTY ownership uniquely buys, and what Windows already gives you for free
How a persistent terminal session actually works on Windows ConPTY's startup handshake, the flags that mislead, who owns the handle, the nesting hazards, and PowerShell probes for every one of them
Ten axes for placing any tool, including ones that do not exist yet The checks that outlive this census, a placement matrix, a worked example, and seven traps that will misclassify a tool
The seven standalone daemons psmux, Zellij, rmux, herdr, oly, qscreen, quil — install commands, transports, and each one's characteristic failure
Three that live inside a larger product wezterm-mux-server, OpenCode's undocumented /pty API, and ao pty-host — working code you extract rather than install
The PTY-as-a-service lineage terminado, ttyd, upterm, gotty, Eclipse Theia, ghostel, and the two reference implementations worth reading even though you cannot run them
Interface shapes: the eight ways a middle layer can be driven CLI verbs, tmux control mode, binary wire protocols, HTTP/WS, MCP, JSON-RPC, SSH, and the no-PTY NDJSON channel
The verb set an ADE-agnostic middle layer has to expose The ten verbs every candidate converged on independently, plus namespacing and auth
The convergence: three ecosystems, one shape A coding agent, a MUD engine and a Lisp REPL arriving at the same architecture
What does not exist The standards, ports and guarantees you would otherwise spend a week searching for
The column that can disqualify every PTY candidate at once "Verbs with no console attached" — untested everywhere, two hours to settle
Comparison tables Three of them: PTY owners, the PTY-as-a-service lineage, and the POSIX-only ancestors
What this does not solve Containers, late attach, the node-pty defects live in ORCA today, unauthenticated local IPC, and the platform's own limits
The playbook: what to pick and how to verify The decision that matters most, then situation by situation
The verification ritual The five steps that prove a session really survived. A success message from the tool proves nothing
Open questions Twenty-six, each with a next step, a cost, and what the answer changes — plus the order to run the first three in
Coverage gaps The inferences flagged as inferences, and everything nobody has run
Primary sources by category Grouped so you can re-verify without redoing the research

How a persistent terminal session actually works on Windows

Before any tool in this guide makes sense, you need the mechanism. Almost every wrong intuition about middle layers on Windows comes from importing a Unix mental model that does not apply.

On Unix, a pseudo-terminal is a pair of file descriptors created out of /dev/ptmx. The master end is a file descriptor like any other: you can dup it, pass it over a Unix socket to another process, inherit it across fork, and hand ownership of a live session from one program to another. On Windows there is no such pair. CreatePseudoConsole returns an HPCON — an opaque handle to a console host process that the OS spawns on your behalf (a headless conhost.exe, or OpenConsole.exe if you bundle one), which sits between your two anonymous pipes and the child, translating a VT byte stream into classic Windows console API calls and back. Three properties of that arrangement generate every consequence in this section: the console is a process, not a file; the handle belongs to whoever created it, and is not something you pass around; and the console is a multi-client object that several processes can be attached to at once. Get those three right and the rest of the Windows story is derivable.

Why the POSIX multiplexers cannot be ported

You will not find tmux on native Windows, and you will not find a port of it. What the census contains is reimplementations of tmux's verb set — psmux (a Rust tmux-workalike), rmux (90+ tmux-compatible verbs) — written from scratch on ConPTY. That is not an accident of nobody trying; it falls out of the three properties above, and the ancestors say so in their own words. GNU screen's original beta-test README states the dependency directly: "Since 'screen' uses pseudo-ttys, the select system call, and UNIX-domain sockets, it will not run under a system that does not include these features of 4.2 and 4.3 BSD UNIX". That file is preserved as an artifact in Orc/screen — and two provenance details are worth stating, because the usual retelling gets both wrong: the file carries no date, and the distribution channel it names is mod.sources, not net.sources. dtach's README is narrower and just as fatal: it "assumes that the host system uses POSIX termios, and has a working forkpty function available". And reptyr — the POSIX answer to the structurally identical problem of grabbing the I/O of a process you did not spawn, via ptrace — has no Windows analogue after 10+ years, and the research located no attempted port; read that as evidence the platform makes it hard rather than as proof the niche is empty, because absence of a port is absence of evidence.

The three missing pieces, concretely: there is no forkpty; there is no master fd to hand to a second process, only an HPCON bound to its creator; and there is no SIGWINCH (see the fourth item below). The causal chain from those three to "no port exists" is reasoning, not a maintainer's statement — but each link is verified independently, and every Windows-native candidate in this guide behaves exactly as the chain predicts.

AttachConsole, and what "multi-client object" buys you

A Windows console is a shared object. Multiple processes attach to one console; GetConsoleProcessList enumerates them; and AttachConsole(pid) lets a process with no console of its own join the console that some other process is already using. That is the entire reason a "late attach" story looks plausible on Windows when it is impossible on Unix — the object you would need to join genuinely is joinable.

Two hard constraints bound it, and both are documented rather than folklore. A Win32 process can own only one console at a time, which is why the single working late-attach control loop found anywhere (reubenlavin08/cc-discord-remote, MIT, 0 stars, dormant since 2026-06-08, one 159 KB bot.py) spawns a throwaway console_helper.py per operation rather than corrupt its own stdio. And reading is gated by integrity level: in microsoft/terminal#5468 — created 17:14:15 and closed Resolution-By-Design at 17:21:46, seven minutes later — DHowett-MSFT wrote "A lower-integrity process is not allowed to read the console output of a higher-integrity console. In general, there is no reason to ever use the ReadConsoleOutput API." Note the rule is directional: same-user, same-integrity siblings (which an ADE's children are) are unaffected; it bites only for elevated agents, cross-user, or session 0.

Microsoft's stance on the write half is blunter still, and the attribution matters because it comes from two separate callouts on the WriteConsoleInput page. The Important banner calls the API "console platform functionality that is no longer a part of our ecosystem roadmap". A separate Tip in Remarks says: "This API is not recommended and does not have a virtual terminal equivalent… This operation is considered the wrong-way verb for this buffer. Applications remoting via cross-platform utilities and transports like SSH may not work as expected if using this API." The classic-vs-VT page names the class: input injection and output scraping "provide a vector to cross security and privilege-levels or domains". AttachConsole itself survives in the still-condoned exceptions list, for process bookkeeping.

The characteristic failure of the late-attach path is that it half-works, convincingly, and then does not. pywinauto#492, filed by a pywinauto maintainer in 2018 and still open, records his own 2022 empirical attempt: AttachConsole succeeded, ReadConsoleOutputCharacter returned ten spaces, and it did not work at all against cmd.exe or PuTTY. The mature implementations each pick one half and never both: NVDA late-attaches but only ever reads (winConsoleHandler.py, AttachConsole(processID) + GetConsoleProcessList(2) + ReadConsoleOutputCharacter), and it is a shipping, actively-maintained screen reader rather than a demo — the code claim is what the repository link supports; how widely deployed NVDA is has no source here. winpty and wexpect read and write but own the console from birth, so they never call AttachConsole at all. Every half has a production implementation and the halves never combine — which is the shape of the finding, rather than a reusable library you can pick up. cc-discord-remote's own notes fill in the rest: ReadConsoleOutputCharacter sees only the visible viewport, never scrollback; concurrent human typing plus programmatic writes garble the transcript, so it enforces "one client per session at a time"; and "the JSONL format and session-registry layout are undocumented internals". It documents its constraints as workarounds rather than solutions, which is what makes it worth citing for engineering notes and not worth taking as a dependency.

And winpty deserves a paragraph of its own, because its life story is the argument for ConPTY. rprichard/winpty (1,380 stars, MIT, last push 2024-02-19) is the pre-ConPTY answer: ReadConsoleOutputW plus WriteConsoleInputW, wrapped into a reusable read-and-write loop and exposed over named pipes — exactly the shape a middle layer wants. It shipped for a decade inside Git for Windows, MSYS2, Cygwin, pywinpty and node-pty's winpty backend, and it is now dormant and superseded. Its existence-and-supersession is the cleanest single argument for building on ConPTY rather than on console scraping. One live residue is worth knowing about because it is a real escape hatch: VS Code's own workaround for the node-pty AttachConsole crash is the setting terminal.integrated.windowsEnableConpty: false, which falls back to winpty. If you ever see that setting recommended, that is what it does.

Also worth naming rather than conflating: late attach is not a nested topology at all. It is an out-of-band, sideways-reaching daemon — a different shape from every other option in this guide.

Who owns the session: whoever called the PTY-spawn API

Whoever calls CreatePseudoConsole owns the HPCON, and the HPCON holder is the only party that can resize the console. There is no transfer, no fd-passing, no adoption. Every working implementation in the census — psmux, Zellij, rmux, herdr, oly, qscreen, quil, wezterm-mux-server, ao pty-host, upterm — spawns its own child and holds the handle from birth. That is not a stylistic convergence; it is the only pole that works.

One apparent counterexample deserves killing, because it gets re-derived every time someone re-researches this: node-pty was thought to prove late-attach works. It does not. In src/windowsPtyAgent.ts:149, this._innerPid = connect.pid; — the PID it hands to AttachConsole is the process node-pty spawned itself. It re-enters its own child's console from a disposable fork. It is another own-from-birth system.

The consequences for you are three, and they are the reason the rest of this guide is shaped the way it is:

  • Insertion has to happen at spawn time. If your middle layer is going to own the PTY, the ADE must launch it — which is exactly what an overridable per-agent launch command such as ORCA's agentCmdOverrides (src/shared/types.ts, consumed at src/shared/tui-agent-launch-command.ts:30) is for. There is no supported way to slide underneath an agent that is already running.
  • Late attach is a read-only escape hatch at best, per the evidence above. It is fine as an out-of-band inspection trick; it is not a foundation.
  • Two boundaries are believed to block it entirely, and both are inference. That a foreign-SID console is not AttachConsole-able, and that the session-0 boundary blocks AttachConsole (relevant if you were thinking of hosting the middle layer as a Windows Service, which runs in session 0 as SYSTEM while you and the ADE run in session 1+), are both inferred from two documented facts — console objects are per-session, and the call takes a PID — and neither has ever been observed to fail. Treat them as strong priors, not as measurements. The service route has a second, established problem anyway: agent CLIs authenticate from the user profile (~/.claude/, ~/.codex/auth.json), which a SYSTEM service does not have.

The four things a middle layer must get right on Windows

This is the build spec. Get these four wrong and the layer either stalls, hangs, renders blank, or silently runs at the wrong size forever.

1. Answer ConPTY's startup handshake, or eat multi-second stalls. When you create a pseudo-console, you are the terminal on the other end of those pipes, and ConPTY expects you to behave like one. It opens by sending DA1 — the CSI c "primary device attributes" query, meaning "what kind of terminal are you?" — and it waits for your reply.

Citation status first, because it changes how you should read the quotes. All of the quotes below are verbatim and were read in full, but they live in comments on microsoft/terminal#7019, which is closed as not_planned since 2023-09-29, was created 2020-07-22, and is titled "conpty exhibits pathological performance on scrolling region redraw (repaints entire screen)". Post-closure comments from maintainers are legitimate evidence; the state is disclosed so nobody mistakes this for an open, tracked commitment.

lhecker (MSFT), in a comment dated 2026-06-10: "it sends you a DA1 request, which you're expected to respond to, because you're using a PTY, not just setting up any random pipes. It waits 3s for a response. It's not multiple queries with 1s waits each." DHowett (MEMBER), in the same issue: "If you have the ability to answer them, answer them. If you do not… that would mean you are not a terminal emulator."

A measured data point in the same thread took startup from 2121 ms to 142 ms by answering instantly. That measurement is a third-party application developer's (the commenter Eyalm321), not a Microsoft benchmark — a distinction that is easy to lose by filing it among the Microsoft attributions.

Characteristic failure: every single spawn costs about two seconds instead of about 140 ms, and it looks like general slowness rather than a protocol bug, so it gets blamed on the agent CLI.

Of the small candidates, psmux verifiably answers the handshake — it has a dedicated regression test at tests-rs/test_cpr_responder.rs.

oly is widely credited with answering it too, and reading the code shows that is wrong in the case that matters. The function usually cited is extract_query_responses_no_client, in src/session/pty.rs, and it does answer terminal-capability queries with no client attached — but it answers CPR, DSR and the OSC 10/11 colour probes, and its own comment lists what it deliberately excludes: "Do NOT respond to: PrimaryDeviceAttributes (DA1), SecondaryDeviceAttributes (DA2), XtVersion (XTVERSION), DecPrivateModeReport (DECRPM), KittyKeyboard… These should only be answered by a real terminal or interactive client. Answering them in detached mode can cause interference with user input and corrupt the output stream." There is a unit test asserting exactly that. oly can emit a DA1 reply — the generator produces \x1b[?62;c — but not on the detached path. DA1 is the query that costs you the three seconds, so on oly's detached path the stall is not answered away; it is deliberately left to whoever is upstream. That is a defensible design choice about not corrupting a byte stream, and it is not the same claim as "oly answers ConPTY's startup handshake". Do not carry the shorter version.

Nothing is known either way about the other five daemons. This is the single most load-bearing design requirement of the four: get it wrong and every spawn costs seconds, or hangs.

2. Do not set PSEUDOCONSOLE_INHERIT_CURSOR unless you can answer ESC[6n fast. ESC[6n is CPR, the cursor-position report request; the expected reply is CSI row ; col R. The flag is value 0x1, and it is one of only three dwFlags consumers in current ConPTY — three read sites spanning four bits, 0x1, 0x08, 0x10 and 0x20 (see the next subsection). The clearest statement of the hazard is in psmux's vendored PTY layer, crates/portable-pty-psmux/src/win/psuedocon.rs:125-133 — the misspelled filename is the source's, the Windows symbol is spelled PSEUDOCONSOLE_INHERIT_CURSOR — which deliberately drops the flag and carries a dedicated regression test. Its comment: "With it, conhost emits an ESC[6n cursor-position request at startup and will not service a child's console connection until the host answers it. So if that reply is sent later than the child's connect attempt, the child blocks in ConsoleCreateConnectionObject during process initialization (a single thread, before any user code runs) until the reply arrives: a temporary stall if it is merely late, indefinite if it never comes."

Characteristic failure: the agent never starts and never prints anything, because it is wedged inside process initialisation before a line of its own code runs — there is nothing to attach a debugger to and nothing in its logs. WezTerm sets this flag; node-pty sets it when inheritCursor is passed. If your middle layer sets it, you have taken on a hard real-time obligation to answer CPR before your child's connect attempt.

3. Load a bundled conpty.dll by absolute path, or not at all. psmux refuses sideloading entirely, and its comment at psuedocon.rs:45-51 explains why: "terminal emulators like WezTerm bundle their own conpty.dll + OpenConsole.exe, and the DLL search order can pick those up when psmux runs inside such a terminal. Using a foreign conpty.dll causes blank panes and broken I/O."

The hazard is real but specific to how you load, and the census splits cleanly on that. Hijackable: WezTerm's portable-pty uses a bare relative name, Path::new("conpty.dll"). Safe: quil (filepath.Join(filepath.Dir(exe), "conpty.dll")) and node-pty (PathCombineW(currentDir, L"conpty\\conpty.dll")) both resolve absolute paths. Note node-pty resolves a different export name on the bundled path (ConptyCreatePseudoConsole rather than kernel32's CreatePseudoConsole), and that ClearPseudoConsole is bundled-only.

Characteristic failure: blank panes and broken I/O, appearing only when your layer happens to be launched from inside a terminal that ships its own ConPTY — so it reproduces on one developer's machine and not another's.

Bundling is still the right call, but for a soberer reason than "Microsoft endorses it". In the same #7019 thread (again: closed not_planned since 2023-09-29), DHowett's recommendation of the ConPTY NuGet package is a refusal to backport: "I'm sorry. When we backport… The architectural shift in ConPTY as of 1.22 is too broad for us to adequately contain. Use the NuGet package; it updates way faster than Windows." The instruction is the same — bundle it — but the reason is that fixes will not reach the in-box ConPTY, which is a stronger argument for bundling and a weaker one about blessing. (Precision on the citation: that comment says only "the NuGet package". The package identifier usually quoted alongside it, Microsoft.Windows.Console.ConPTY, does not appear anywhere in #7019 and carries no separate source here — the refusal-to-backport reading is what the thread supports; confirm the package name on NuGet before you put it in a manifest.) quil (scripts/fetch-conpty.sh, SHA256-pinned), node-pty (third_party/conpty, 1.25.260303002), WezTerm and ghostel all bundle it.

Two live caveats on the bundled path. The ABI has drifted: node-pty declares ConptyClearPseudoConsole(HPCON hPC) while microsoft/terminal src/inc/conpty-static.h:44 declares ConptyClearPseudoConsole(HPCON hPC, BOOL keepCursorRow) — a one-argument call against a two-argument export, on the useConptyDll path. Drift runs both ways: node-pty also declares ConptyClosePseudoConsoleTimeout, which the current public header does not export. And no compatibility matrix exists, from Microsoft or anyone, for running a newer bundled conpty.dll / OpenConsole.exe against an older or locked-down Windows Server Core or LTSC host.

4. Resize does not propagate by itself — there is no SIGWINCH analogue. ResizePseudoConsole(HPCON, COORD) is an explicit call made by whoever owns the handle. A child console app only learns about it by reading a WINDOW_BUFFER_SIZE_EVENT off its console input buffer — which a byte-stream-only consumer never sees. So resize is a message you must route yourself, at every hop, or it silently does not happen.

Two shipping solutions exist, and they take opposite approaches:

  • psmux's pipe mode asks the outer terminal in band, using XTWINOPS CSI 18 t ("how big are you?"), waiting 500 ms for a CSI 8 ; rows ; cols t reply and falling back to a hardcoded 120x30 if none arrives (src/ssh_input.rs:2016-2039, src/main.rs:4112-4130).
  • Zellij polls. It calls crossterm::terminal::size() every 50 ms on the VT-reader path (zellij-client/src/os_input_output_windows.rs:95 doc-comment, :176 sleep), and separately runs a 100 ms poller at :38/:52.

Characteristic failure, and it is a silent-corruption class rather than a cosmetic one: whether Electron/xterm.js answers CSI 18 t by default is unknown — xterm.js gates windowOptions behind opt-in flags. If it does not answer, psmux's pipe mode runs at 120x30 forever and every TUI inside it wraps against a size nothing on screen agrees with.

The purest instance of the failure is Watfaq/PowerSession-rs, an asciicast recorder on ConPTY (MIT, 295 stars, pushed 2026-07-24, Windows-only): it calls CreatePseudoConsole once, sized from GetConsoleScreenBufferInfo at startup, and never calls ResizePseudoConsole anywhere — a repo-wide symbol search returns zero hits, so a mid-recording resize of the outer pane cannot propagate. Split the evidence from the citation on that one, because they are at different strengths: the finding was made by reading the ConPTY backend at source, but the exact file and line were not recorded when it was made and could not be recovered afterwards. Treat the behaviour as verified and the pointer as missing. Two more things about that project generalise past it: its own open issue #272 records that it may not honour ASCIINEMA_SERVER_URL and that upload is not gated behind auth; and more importantly, a recorder captures everything that transits the PTY, including secrets. That warning applies to every scrollback-replay and session-recording candidate in this guide, not just to this one.

The node-pty ABI drift in item 3 bears on this item too: a stale one-argument prototype against a two-argument export is exactly the kind of mismatch that surfaces as a resize or a clear that silently does nothing.

The creation flags: one live, two dead, one actively harmful

You will be tempted to copy a flag list from another project. Do not, without checking it against the current implementation, because three of the commonly-copied bits are wrong today.

The complete set of dwFlags consumers in microsoft/terminal src/winconpty/winconpty.cpp is three lines:

169: const auto inheritCursor   = (dwFlags & PSEUDOCONSOLE_INHERIT_CURSOR)   ? L"--inheritcursor "   : L"";
170: const auto ambiguousIsWide = (dwFlags & PSEUDOCONSOLE_AMBIGUOUS_IS_WIDE) ? L"--ambiguousIsWide " : L"";
173: switch (dwFlags & PSEUDOCONSOLE_GLYPH_WIDTH__MASK)

A code search over the repository returns zero hits each for PSEUDOCONSOLE_RESIZE_QUIRK, PSEUDOCONSOLE_WIN32_INPUT_MODE and PSEUDOCONSOLE_PASSTHROUGH_MODE. The current public flag list in src/inc/conpty-static.h is INHERIT_CURSOR (0x1), GLYPH_WIDTH__MASK 0x18 / GLYPH_WIDTH_GRAPHEMES 0x08 / GLYPH_WIDTH_WCSWIDTH 0x10 / GLYPH_WIDTH_CONSOLE 0x18, and AMBIGUOUS_IS_WIDE (0x20). Three consequences:

  1. Passing 0x2 | 0x4 is a no-op. A middle layer on portable-pty confers no flag advantage over an ADE on node-pty. If you have read somewhere that it does, delete the claim.
  2. Passing 0x8 is actively harmful. switch (dwFlags & 0x18) matches case PSEUDOCONSOLE_GLYPH_WIDTH_GRAPHEMES (0x08), so anything still ORing the stale PASSTHROUGH_MODE value silently selects grapheme-based width measurement. The dead symbol appears in 148 files in a global GitHub code search for PSEUDOCONSOLE_PASSTHROUGH_MODE — including psmux, Sora-bluesky/winsmux, aws/amazon-q-developer-cli and vibetunnel — so it is propagating through the exact candidate pool you are shopping in. (That count is a snapshot from one search on one date and no query string or platform was recorded with it; re-run the search rather than quoting the number.)
  3. win32-input-mode as a creation flag is dead; as a mode it is alive. It is negotiated in band at runtime with CSI ?9001h, not requested at creation.

The method rule this produced, and it generalises: when you find a flag or a #define, check the consumer, not the header. Absence from a public header proves only "undocumented" — 0x2 and 0x4 are equally absent from that header and were treated as live features for years. Presence in a header proves nothing about whether anything reads it.

Nesting: your middle layer runs inside the ADE's ConPTY

This is the topology you are actually buying into: the ADE spawns your middle layer through node-pty (so, a ConPTY), and your middle layer spawns the agent through a second ConPTY. Five things about that stack behave differently from the single-layer case.

Nesting itself is a designed, negotiated scenario, not an accident. Microsoft's own keyboard-handling spec (microsoft/terminal doc/specs/#4999 - Improved keyboard handling in Conpty, lines 299-322) works through the chain WT → conpty[1] → wsl → conpty[2] → cmd.exe: "Conpty[2] will ask for win32-input-mode from conpty[1] when conpty[2] first boots up. As conpty[1] is just a conhost that knows how to handle win32-input-mode, it will switch its own VT input handling into win32-input-mode." The same spec confirms the cost is by design: each layer re-encodes INPUT_RECORDs to VT and back.

Double VT parsing is no longer an OS-level problem, and the conclusion that it is has been retired. microsoft/terminal PR #17510, merged and closed 2024-08-01T20:38:11Z under the title "A minor ConPTY refactoring: Goodbye VtEngine Edition", states: "any VT output that an application generates will now be given to the terminal unmodified." Issue #1173 closed as completed at 2024-08-01T20:38:13Z — two seconds later. Since August 2024, ConPTY passes application VT output through unmodified by default.

What is still lost at each hop is your own doing, and it is a design choice you make. There are three poles for terminal-state ownership, and only one of them costs you fidelity.

  • An opaque relay keeps no screen model. dtach's README states it plainly: "dtach does not have a terminal emulation layer, and passes the raw output stream of the program to the attached terminals." Nothing is lost in transit; nothing can be redrawn on reattach either.
  • A full VT owner parses every escape into a grid and re-emits rendered ANSI. screen, tmux, Zellij, psmux's TUI mode and herdr are here. This is the pole where a nested layer becomes a second renderer.
  • Parallel model with a raw live path keeps a VT model for snapshots, resize bookkeeping and scrollback while shipping raw child bytes on the live path: qscreen's AttachMode::Bytes (crates/qscreen-protocol/src/lib.rs:56-61), oly's ServerMessage::Data { data: Vec<u8> } (src/http/ws.rs:36-46), quil's PaneOutputPayload { Data []byte } (internal/ipc/protocol.go:204-208), and psmux's -CC %output pane-output ring (src/server/mod.rs:1222-1227).

Vocabulary, once, because it recurs: -CC is tmux's control mode — a line-based machine protocol in which the client sends tmux commands on stdin and the server answers with %begin / %end / %output lines on stdout, so the outer program owns rendering while tmux owns sessions. It gets its own treatment among the interface shapes below.

The test that places any tool: does the live-path payload type carry bytes / Vec<u8> / []byte, or a string? In the third pole the ADE's own xterm.js can be the only VT parser in the chain while the daemon still replays scrollback on reattach.

One entry in that third pole needs its asterisk stated here rather than buried. psmux's -CC %output ring is octal-escaped, which is a byte-oriented encoding, and that is why it is placed in the third pole. But the drain that feeds the escaping runs String::from_utf8_lossy(&bytes) first, at src/server/mod.rs:1225the bytes are already destroyed before they are escaped. So psmux -CC is in the third pole by design intent and in the second pole by the strict payload-type test. Where this guide later tells you to prefer wires that carry bytes, psmux -CC does not qualify on the strict reading, and you should know which reading you are relying on.

String-typed wires lose bytes, permanently, and this is the most under-appreciated failure in the field. psmux does String::from_utf8_lossy(&bytes) on each ring drain before escaping (src/server/mod.rs:1225), so a multi-byte character split across two drains becomes U+FFFD and stays that way. The same class of bug is in OpenCode's /pty wire (packages/core/src/pty/protocol.ts): its frames are described as "raw UTF-8 terminal chunks" but the payload type is a JS string, and chunks(data: string) slices at REPLAY_CHUNK = 64 * 1024 using String.slice — i.e. UTF-16 code units, so a surrogate pair straddling a replay-chunk boundary is split into lone surrogates; meanwhile decodeInput uses new TextDecoder("utf-8", { fatal: true }) inside a try { … } catch { return undefined }, so invalid UTF-8 on the input path is silently dropped, as the file's own comment says. This is a property of string-typed terminal transports generally, not one project's defect. It matters here because agent CLIs emit heavy Unicode: the in Claude Code's own window title already broke quil's emulator.

Control-mode corruption — the finding, with its antecedent restored. psmux's maintainer, in src/main.rs:4188-4193: "When running over SSH with a ConPTY console, Windows ConPTY silently consumes DCS escape sequences (including the \x1bP1000p that iTerm2 uses to detect tmux control mode) and also interleaves its own cursor positioning sequences into the output, corrupting the line-based protocol." The remedy the source gives is likewise SSH-specific: "the SSH client must disable PTY allocation so that stdin/stdout are raw pipes: ssh -T user@host tmux -CC."

The generalisation is inference, not the source's claim, and it should be labelled as such wherever it is repeated. The mechanism — ConPTY eating DCS and injecting cursor sequences — plausibly generalises to any ConPTY sitting between a -CC server and its client, including a node-pty one. The source does not say that. Two independent pieces of evidence make the inference a reasonable prior rather than a guess: lhecker's note on #7019 that passthrough-mode ConPTY "already injects VT sequences into stdout irrespective of the VT parser state", and microsoft/terminal#19621, filed by DHowett (MEMBER) and open: "This will impact tmux control mode, as ConPTY will send \x1B to terminal and promptly terminate the in-flight exchange. All in-band signaling from ConPTY must be suppressed when passing through a DCS." So passthrough — the fix for double parsing — makes this particular bug worse.

Test whether the cure is reachable before you test whether the disease exists. This is the canonical statement of that rule; everywhere else in this guide points back here. The documented remedy for -CC corruption is "give the child raw pipes". An Electron ADE that spawns through node-pty spawns through a ConPTY, always. So the prior question is not "does ConPTY corrupt -CC in my topology" but "can an overridable launch command produce a raw-pipe child at all?" — because if it cannot, the corruption question is moot, -CC control mode is simply unavailable in this topology regardless of what a corruption test would have found, and psmux drops to its plain CLI verbs. Two hours of reading ORCA's spawn path settles the prior question (Q1a); an afternoon settles the corruption question (Q1); and the cheap one can make the expensive one unnecessary. Both are in Open questions below.

Second-order, and a straight hit against cross-platform uniformity: psmux's own docs/control-mode.md:391 records that "ConPTY may normalize line endings and process certain cursor movement sequences internally. %output data may look slightly different from what a Unix tmux session would produce."

Resize desync between layers is unfixable from outside, per Microsoft. microsoft/terminal#15976 was promoted to a megathread — "we're promoting this to a megathread cause this is a Hard problem" — and the in-process ConPTY spec (doc/specs/#13000, lhecker) names it "unsolvable" in the current architecture: the drift is between ConPTY's buffer and the hosting terminal's buffer, two separate processes, and a named MUTEX is explicitly rejected on ABBA-deadlock grounds. The consequence for you is direct: a nested middle layer that keeps its own screen model becomes a third independent representation of the same screen.

Prefix keys are the least-researched nesting concern, and the honest answer is that nobody knows. The question is whether a multiplexer's prefix chord eats the ADE's keybindings, and whether it can be rebound or disabled with no human attached.

Candidate Has a prefix? Rebindable? Disable-able for headless use?
psmux Yes, tmux-style C-b Inferred yes — tmux-compatible config implies set prefix, not verified Unknown
Zellij Yes, Ctrl modes rather than a single prefix Inferred yes — keybindings are a first-class KDL config section Unknown
rmux Yes, tmux-compatible Inferred yes, same reasoning as psmux Unknown
herdr Yes Unknown — but its Windows-beta page lists "Prefix input-source switching | unsupported" (windows-beta.mdx), the only primary-source statement about prefixes on Windows found anywhere in this research Unknown
qscreen, oly, quil, ao pty-host, OpenCode /pty Unknown — single-pane or network-attached designs may have no prefix at all Unknown Unknown
docker attach (as a design reference) Yes, CTRL-p CTRL-q Yes, documented--detach-keys, per-container or global (docs.docker.com/reference/cli/docker/container/attach/) Yes, by remapping to an unused chord

The only documented remapping mechanism in the entire census belongs to docker, not to any multiplexer candidate. Every multiplexer's rebindability above is an inference from "it has a config file" — nobody has set the key and confirmed no chord is intercepted. And for a headless-first middle layer the right target is stronger than rebinding anyway: no prefix at all on the headless path, with the prefix existing only when a human attaches. None of the candidates is known to offer that.

Two more nesting hazards live in the outer layer and are worth knowing before you blame your own code: pasting more than 5 KiB into a slow-reading app deadlocks the whole terminal and stops Ctrl-C from working (microsoft/terminal#17384, open since 2024-06-06); and a handful of VT-passthrough issues remain open in #17643COMMON_LVB_GRID_HORIZONTAL to SGR 53 translation dropped, VtIo::Writer::WriteInfos not verifying per-character width, SetConsoleActiveScreenBuffer destroying grapheme clusters. Cooked-read reflow, ScrollConsoleScreenBuffer, the DA3 truncation and the PSReadLine SGR issue are all fixed — do not carry those forward as hazards.

Finally, a scoping note about all of the above: double-ConPTY nesting has not been empirically tested by any candidate in this research. The mechanism is documented; the behaviour of these specific tools inside a second ConPTY is not. That includes the one candidate widely credited with having nested-agent tests — oly — where the thing that created the impression was not a README but a test fixture: tests/output-copilot.log, a recorded ANSI dump that no live-agent test ever opens. The distinction matters and is set out in the oly entry and in the worked example.

How to check any of this yourself

Everything below runs on native Windows 11 in pwsh, with no build tools. Run each one inside an ADE pane as well as in a plain terminal — the difference between the two answers is the whole point.

Disclosure before you paste any of it. These probes are new code written for this guide, not code taken from any project, and none of them has been executed anywhere. The Win32 semantics behind each one are cited above; the PowerShell wrapping them is not evidence of anything. That is the same standard this guide applies to the install commands, and it applies here too — the "you should see" line under each probe is what turns a run into a result, and if what you see is different, the probe is the first thing to suspect, not the platform.

Is the in-box ConPTY there, and which build are you on? The passthrough behaviour is corroborated at build >= 22621 by two independent Rust implementations converging on the same magic number (psmux and rmux). No primary Microsoft source pins that threshold, and neither implementation's constant was recorded here with a file path, so the corroboration is real but not locatable from this document — treat the build number as a heuristic, not a contract, and grep for 22621 in either tree if you need the citation.

$sig = @'
[DllImport("kernel32.dll", CharSet=CharSet.Unicode, SetLastError=true)]
public static extern IntPtr GetModuleHandleW(string name);
[DllImport("kernel32.dll", SetLastError=true)]
public static extern IntPtr GetProcAddress(IntPtr module, string proc);
'@
$k = Add-Type -MemberDefinition $sig -Name Conpty -Namespace Probe -PassThru
$h = $k::GetModuleHandleW('kernel32.dll')
'CreatePseudoConsole','ResizePseudoConsole','ClosePseudoConsole','ClearPseudoConsole' |
  ForEach-Object { '{0,-22} {1}' -f $_, ($k::GetProcAddress($h, $_) -ne [IntPtr]::Zero) }
"OS build: $([Environment]::OSVersion.Version.Build)"

You should see: True for the first three, and False for ClearPseudoConsole — that is the correct, healthy answer, not a failure. ClearPseudoConsole is a bundled-only export, as item 3 above establishes; kernel32 does not export it, and the bundled conpty.dll spells it ConptyClearPseudoConsole anyway. A machine that answers True on that row would be the surprising one.

See the multi-client console for yourself. This is GetConsoleProcessList — the same call NVDA uses for liveness. Run it in a plain console, then inside an ADE pane, then inside a nested multiplexer, and watch the attached set change.

$sig = @'
[DllImport("kernel32.dll", SetLastError=true)]
public static extern uint GetConsoleProcessList(uint[] list, uint count);
'@
$k = Add-Type -MemberDefinition $sig -Name Con -Namespace Probe2 -PassThru
$buf = New-Object uint32[] 64
$n = $k::GetConsoleProcessList($buf, 64)
"attached processes: $n"
if ($n -gt 0) {
  $buf[0..($n - 1)] | ForEach-Object { Get-Process -Id $_ -ErrorAction SilentlyContinue } |
    Select-Object Id, ProcessName
} else {
  "no console attached (this is the console-less case; GetConsoleProcessList returned 0)"
}

You should see: a small count — typically 1 or 2 in a plain console (the shell, plus its host on some configurations), and more inside an ADE pane or a nested multiplexer, which is the point of running it in all three places. The if ($n -gt 0) guard is load-bearing, not defensive tidiness: in PowerShell 0..-1 evaluates to the sequence 0, -1, so an unguarded $buf[0..($n-1)] on a zero result indexes element 0 and element 63 and prints two garbage PIDs — precisely in the console-less case you most want a clean answer for.

Does whatever is above you answer DA1 and CSI 18 t? This is the cheapest way to find out whether you will pay the 3-second stall, and whether psmux's in-band sizing can work in your ADE. Save it as da1-probe.ps1 and run it as a separate process rather than pasting it into a live session — PSReadLine competes for input and the reply characters vanish before your read loop sees them. It only works in an interactive session: [Console]::KeyAvailable throws when stdin is redirected.

# da1-probe.ps1
$e = [char]27
function Ask([string]$seq, [int]$ms = 500) {
  while ([Console]::KeyAvailable) { [void][Console]::ReadKey($true) }   # drain
  [Console]::Write($seq)
  Start-Sleep -Milliseconds $ms
  $r = ''
  while ([Console]::KeyAvailable) { $r += [Console]::ReadKey($true).KeyChar }
  if ($r -eq '') { '(no reply)' } else { $r -replace [regex]::Escape([char]27), '<ESC>' }
}
"DA1   ESC[c    -> " + (Ask "$e[c")      # expect something like <ESC>[?1;0c
"SIZE  ESC[18t  -> " + (Ask "$e[18t")    # expect <ESC>[8;<rows>;<cols>t
pwsh -NoProfile -File .\da1-probe.ps1

A note on "$e[c", because it looks like a bug and is not: PowerShell does not expand index syntax inside an expandable string, so $e interpolates and [c is literal. "Fixing" it to "$e[c]" or "${e}[c" changes what is written to the terminal — leave it alone.

You should see: on the DA1 line, something of the form <ESC>[?1;0c or <ESC>[?62;...c — any reply at all is a pass. (no reply) means nothing above you is behaving as a terminal, and a child that waits on DA1 will burn its full 3 seconds on every spawn. On the size line, <ESC>[8;<rows>;<cols>t is a pass; (no reply) run inside an ADE pane means xterm.js is not answering XTWINOPS and psmux's pipe mode would sit at 120x30 forever.

Does resize actually reach the child? Run this inside an ADE pane and drag the pane border.

while ($true) { '{0}x{1}' -f [Console]::WindowWidth, [Console]::WindowHeight; Start-Sleep 1 }

You should see: the printed dimensions change within a second of the drag, and match the pane you are looking at. The correct number is whatever your pane actually is — the test is that it tracks, not that it hits a particular value. Numbers that never change mean ResizePseudoConsole is not being called on that hop, which is item 4's failure, live. Numbers that change but settle on 120x30 are the psmux in-band-sizing fallback, which is a different failure with the same symptom.

Which input path will a multiplexer choose? Zellij's Windows input selection keys off TERM and WT_SESSION; when neither is set it picks the native-console path, which is implemented and conservative. This is a config check, not a hazard — but you want to know the answer before you debug anything else.

"TERM='$env:TERM'  WT_SESSION='$env:WT_SESSION'  ConEmuANSI='$env:ConEmuANSI'"

You should see: all three empty inside an ADE pane, and WT_SESSION populated inside Windows Terminal. Empty is the answer that means Zellij takes its native-console INPUT_RECORD path.

Does late attach reach an agent your ADE spawned? Save this as attach-probe.ps1. It calls FreeConsole first, which destroys the caller's own console — that is why it must run as a throwaway process with its output redirected to a file, and it is the same "one console per process" constraint that forced cc-discord-remote into a helper-per-operation design.

# attach-probe.ps1  —  usage: pwsh -NoProfile -File .\attach-probe.ps1 1234
$sig = @'
[DllImport("kernel32.dll", SetLastError=true)] public static extern bool FreeConsole();
[DllImport("kernel32.dll", SetLastError=true)] public static extern bool AttachConsole(uint pid);
[DllImport("kernel32.dll", SetLastError=true)] public static extern uint GetConsoleProcessList(uint[] l, uint c);
'@
$k = Add-Type -MemberDefinition $sig -Name A -Namespace Probe3 -PassThru
[void]$k::FreeConsole()
$ok  = $k::AttachConsole([uint32]$args[0])
$err = [Runtime.InteropServices.Marshal]::GetLastWin32Error()
$buf = New-Object uint32[] 64
$n   = if ($ok) { $k::GetConsoleProcessList($buf, 64) } else { 0 }
"attach=$ok err=$err attached=$n"
# Find the agent's PID, then probe it from a separate process.
# Note: no angle brackets anywhere — in PowerShell `<` is a reserved redirection
# operator and a bare <PID> placeholder is a parse error, not a blank to fill in.
Get-Process claude, node, pwsh -ErrorAction SilentlyContinue | Select-Object Id, ProcessName
$targetPid = 1234                      # <-- put the agent's PID here
pwsh -NoProfile -File .\attach-probe.ps1 $targetPid > "$env:TEMP\attach-probe.txt" 2>&1
Get-Content "$env:TEMP\attach-probe.txt"

You should see: attach=True err=0 attached=N with N at least 1 on a successful attach, or attach=False err=5 (access denied) / err=6 (invalid handle) on a refusal — and the error number is the useful part, because it tells you which boundary stopped you.

A successful attach tells you the boundary is crossable in your topology. It does not tell you that reading works — remember pywinauto's ten spaces — so if you care, follow it with a ReadConsoleOutputCharacter call and compare what comes back against what is on screen. And keep the general rule in mind while reading any of these results: a tool reporting success proves nothing. A CreatePseudoConsole that returned S_OK, a spawn that printed a prompt, and a resize call that returned without error are all compatible with a session that is silently the wrong size, silently losing bytes to a lossy string conversion, and silently three seconds slower per spawn than it needs to be.

Ten axes for placing any tool, including ones that do not exist yet

The census in the next section has a shelf life measured in months — six ADEs died or closed in the eight before this was written, and every star count in it is a snapshot that will be wrong by the time you act on it. The axes below do not rot the same way, because each one asks a question about a design decision rather than about a project's health, and each one comes with a check you can run against a repository you have never opened.

That is the point of this section. When a new multiplexer shows up on Hacker News next spring, you should not have to redo any of this. You should be able to clone it, run ten checks, and know within twenty minutes what it is, whether it can sit under your shell, and which single property it forfeits. A judgement about the census, stated as one: every candidate that survived this survey forfeits exactly one of {maturity, Windows-nativeness, standalone packaging, contract stability} — not one, all of them, and each a different one. That is a reading of eleven tools, not a law about tools in general, and a project written next year is free to falsify it. The axes are how you find out which property a new arrival forfeits before you have built anything on top of it.

Four of these ten axes are the four framing questions, restated as repo-checkable tests. Ownership timing is Owner; persistence scope is Lifetime; transport binding is Rendezvous; terminal-state ownership is Payload. The other six — session/window coupling, namespace model, command-language uniformity, extensibility mechanism, agent-state derivation, substrate stance — are the ones that decide between tools that answered the four identically, which is most of the field.

An axis earns its place here only if a different answer changes what you would build. "Written in Rust" is not an axis. "Ships raw child bytes on the live path versus re-rendered ANSI" is, because one of those means your ADE's xterm.js is the only VT parser in the chain and the other means you are paying for two. Four of these ten replaced earlier formulations that turned out to be false; the last subsection keeps those as standing traps, because each is a mistake a competent engineer makes independently and one of them — reading a Unix socket in source as proof of a POSIX-only tool — is actively wrong on modern Windows.

The ten checks at a glance

Axis The question The fastest check What the wrong answer costs you
Session/window coupling Does one binary own both persistence and layout? Does split / new-window / select-pane exist in --help? You inherit a layout engine you do not want, and cannot swap it
Terminal-state ownership Does the live path carry bytes or a rendered screen? The payload type of the live-output message: Vec<u8>/[]byte vs String A second VT parse, and fidelity lost at every hop
Namespace model Who can collide with you, and who can squat on your endpoint? Read the default socket/pipe name construction, then grep for an ACL Two projects sharing a daemon; a hostile local process harvesting your I/O
Command-language uniformity Do a script, a hook and a human speak the same verbs? Does the same verb string work as tool verb, as a config line, and as a keybinding? Two grammars to learn and to keep in sync when the human leaves the loop
Extensibility mechanism What shape does an extension take? Shell string, wire protocol, plugin binary, embedded interpreter, or typed SDK You write and maintain the client library yourself
Agent-state derivation How do you learn the agent is idle, blocked, or done? Grep for a VT/emulator dependency, and for a regex over screen text Silent stale-buffer misreads with no human to notice them
Substrate stance Does it wrap tmux, replace it, or skip the PTY entirely? Is tmux a prerequisite or a spawned binary? Is there a PTY library at all? A POSIX dependency smuggled in under a cross-platform README
Transport binding What actually carries the IPC, and is it safe on Windows? Grep for the four transport signatures below A daemon that compiles on Windows and an endpoint anyone can read
Persistence scope What exactly survives what? Kill the spawning tree, then try to attach from a new client A "persistent" tool that dies with the ADE — the one thing you are buying
Ownership timing Does it spawn the child or attach to a stranger's console? Whose PID goes into the attach call? Months spent on late-attach, which nothing in the field makes work

Read down a column to compare the field on one decision. Read across a row to characterise one tool. The disqualifying answer usually falls out of a single cell, which is the property that makes this worth doing at all — see the worked example near the end, where a candidate is refuted entirely by the transport cell.

Session/window coupling — does one binary own both jobs?

Pole What it means How to determine it Where the field lands
Monolithic One binary owns session persistence and pane/window layout One executable, and killing it loses both the layout and the sessions screen, tmux, Zellij, psmux, rmux, herdr, boo
Split Persistence and layout are separate, swappable programs The layout program appears as an argument to the session program, and can be replaced with any other command abduco + dvtm
Session-only No layout concept at all No split, new-window or select-pane verb exists anywhere in the CLI dtach, qscreen (single pane by design), upterm

The split pole is the closest existing precedent for what you are shopping for — a session layer that deliberately does not own rendering, because your ADE already owns rendering. abduco's author says it in as many words on the abduco/dvtm writeup: "these are two distinct features: window and session management shouldn't be intermingled." The idea travels; the code does not. Both halves are dormant — abduco's last commit is 2020-04-30, dvtm's 2021-03-06 — and both are POSIX-only.

The practical reading for a middle layer: a monolithic tool is not disqualified, it is over-specified. You will be paying for and nesting a layout engine whose panes you never use, and its layout keybindings become prefix-key collisions inside your ADE's own PTY.

Terminal-state ownership: three poles, and the double parse is optional

The three poles were introduced under Nesting above; here they are as a placement check. The axis's original formulation ("rendering multiplexer versus transparent byte-passer") was a binary and it was wrong: there is a third pole, and most modern candidates sit in it.

Pole Behaviour How to determine it Where the field lands
Opaque relay No screen model; forwards bytes verbatim; cannot redraw on reattach No VT or emulator crate in the dependency manifest at all dtach — its README states it plainly: "dtach does not have a terminal emulation layer, and passes the raw output stream of the program to the attached terminals"
Full VT owner Parses every escape into a grid, re-emits rendered ANSI The live-path payload type is a string or a rendered-cell struct, not a byte slice screen, tmux, Zellij, psmux TUI mode, herdr, ripple's VtLiteState
Parallel model, raw live path Maintains a VT model for snapshot, resize and scrollback bookkeeping, but ships raw child bytes on the live path The live-path payload carries bytes / Vec<u8> / []byte, and a separate VT model exists for snapshots qscreen AttachMode::Bytes (crates/qscreen-protocol/src/lib.rs:56-61); oly ServerMessage::Data { data: Vec<u8> } (src/http/ws.rs:36-46); quil PaneOutputPayload { Data []byte } (internal/ipc/protocol.go:204-208); psmux -CC %output, an octal-escaped pane-output ring (src/server/mod.rs:1222-1227) — on intent only; see the note below the table

The widely repeated "the double parse is unavoidable" framing is false. It is avoidable today, with shipped interfaces, in three of the four small candidates, and all four citations above are verified in source.

The same check demotes candidates that are otherwise among the best-shaped in the census, which is why the axis is worth having. Apply it to OpenCode's /pty and it lands in the second pole, not the third: its outbound frames are described as "raw UTF-8 terminal chunks", but the payload type in packages/core/src/pty/protocol.ts is a JavaScript string, with the surrogate-splitting and silent-drop consequences set out under Nesting. Apply it strictly to psmux's -CC %output and the same thing happens for the same reason: the ring is octal-escaped, which is byte-oriented, but the drain runs String::from_utf8_lossy at src/server/mod.rs:1225 before the escaping, so the escape encodes bytes that have already been through a lossy conversion. Both are placed in the third pole on design intent and fall to the second pole on the strict test. Say which reading you are using whenever you rely on either.

How to run this check on a tool you have never seen: find the message type the server sends on every output tick — not the snapshot type, not the scrollback type — and look at the field's declared type. A String, a text field, or a JSON-encoded payload puts it in the second pole no matter what the README calls it.

Namespace model — who can collide with you, and who can squat on you

Ordered worst to best. The poles are about who can collide with you, not about the transport — those are two different axes, and collapsing them is exactly how oly ends up misplaced.

Pole Mechanism How to determine it Where the field lands
Machine-global default The default name contains no user, SID or project component, so two users on one host collide Read the socket or pipe name construction: is there any per-user or per-project component in the default path? oly — default open-relay.oly.sock, no user component (src/config.rs:123-126)
Single fixed instance, per-user One instance per user by construction, with no way to run two side by side The name embeds a user component but no -L-style label, and no env var overrides it qscreen — \\.\pipe\qscreen-<user> (crates/qscreen-shared/src/lib.rs:6-11)
Socket-name-as-namespace The socket or pipe name is the namespace, selectable per invocation A -L/-S flag or a name env var exists tmux -L/-S, psmux -L/-S, rmux -L, and oly via OLY_SOCKET_NAME
Env-var home split A whole state directory per instance An env var relocates the entire state root, not just the socket quil QUIL_HOME, Claude Code CLAUDE_CONFIG_DIR
Identity-scoped The namespace is derived from user SID plus integrity level, not from a string the caller picks The name is computed from GetTokenInformation-class data, so it cannot be spoofed by argument rmux — \\.\pipe\{prefix}-{sid}-il-{integrity}-{label} (crates/rmux-ipc/src/endpoint.rs)

Identity-scoped is the only pole where the namespace cannot be entered by a process that merely guesses a string, and only rmux is in it. What that buys is specific: it removes an entire class of collision and squatting, and it matters exactly to the degree that some other local process might want in. On a machine where you are the only account and the threat model does not include a hostile local process, this axis stops discriminating between candidates and the choice moves elsewhere. On a shared or multi-user machine it is the first filter. Everything below the top pole depends on the pipe's ACL for actual security — and only rmux and qscreen set one.

The trap this axis sets, and the correction that comes out of it. The easy first reading is that oly's socket name is hardcoded, which would put it at the bottom of the table with no isolation knob at all. It is not hardcoded: src/config.rs:123-126 reads std::env::var("OLY_SOCKET_NAME").ok().and_then(normalize_optional_string).unwrap_or_else(|| "open-relay.oly.sock".to_string()) — an env-var-selectable name with a machine-global default, which is the same pole as tmux. The correction stands, and the real defect is on a different axis entirely: oly's pipe is created with no ACL and no FILE_FLAG_FIRST_PIPE_INSTANCE, so a hostile local process can pre-create it and harvest client connections. The isolation knob exists; the access control does not. The evidence, and what the project says about it, is in the oly entry among the standalone daemons below.

So: check the default, not the constant, and check the ACL separately from the name. Namespacing and access control are different problems, a tool can pass one and fail the other, and reading one string in one file would never have found the defect that matters here.

Command-language uniformity — do a script, a hook and a human speak the same verbs?

Pole How to determine it Where the field lands
One grammar usable from shell, config file, and keybinding The same verb string works as tool verb, as a config-file line, and as a keybinding target tmux (its manual page has a whole "COMMAND PARSING AND EXECUTION" section), psmux, rmux (90+ tmux-compatible verbs)
Separate keybinding table bolted onto a distinct config syntax The config file's grammar and the CLI's grammar do not overlap screen
Discoverability-first on-screen hinting The primary interface is a rendered hint bar, and CLI verbs are secondary Zellij, whose README states the goal as "must not sacrifice simplicity for power"

Why this axis matters specifically for a headless middle layer: the first pole is the only one where a script, a hook and a human all speak the same language, so nothing has to be re-learned when the human leaves the loop. With the third pole you are driving a tool whose designers optimised the path you will never use.

Extensibility mechanism — what shape does an extension take?

Pole How to determine it Where the field lands
Shell hooks and pipes Extension points take a shell command string tmux hooks, pipe-pane, screen backtick
External wire protocol A documented framing exists that a non-child process can speak tmux -CC control mode, psmux -CC, qscreen ScreenFrame, the WezTerm mux codec
Sandboxed compiled plugin runtime Plugins ship as compiled artifacts against a schema'd host API Zellij WASM plugins, protobuf-schema'd since 0.38.0
Embedded scripting runtime An interpreter is linked in and the config file is a program WezTerm (Lua config plus an event API)
Typed SDK A published client library exists in at least one language, with types rmux (Rust, Python and TypeScript, with session / pane / snapshot / wait_for_text), herdr (third-party TypeScript and Python clients)

For your use case the second and fifth poles are the ones that pay. A wire protocol you can speak from any language is what makes the layer ADE-independent; a typed SDK is that plus somebody else maintaining the client. Note that herdr's SDKs are third-party, which means the contract you would depend on is not the one the project promises to keep.

Agent-state derivation — how you learn the agent is idle, blocked, or done

Pole Mechanism Where the field lands Characteristic failure
Vendor-emitted structured events The agent CLI itself emits typed events Claude Code OpenTelemetry plus JSONL transcripts plus stream-json; Codex --json rollout events; OpenCode /global/event SSE Vendor-specific, and the config surface changes under you
Native typed state The multiplexer owns a VT parser and exposes typed peek/wait/snapshot boo wait --idle and peek --json, rmux snapshot(), quil MsgScreenshotPaneResp{Text,CursorX,CursorY}, andyk/ht Only as good as the emulator
External heuristic scraping Regex or OSC-title matching over another program's rendered screen tmux-agent-status, tmuxai, the kiro_cli poller A documented convergent bug — see below

How to determine which pole a tool is in: grep for a VT or emulator dependency, and grep for a regex over screen text. Structured-event consumers have neither. Native-typed-state tools have the emulator. Scrapers have the regex and no emulator.

The scraping failure is documented, not theoretical. awslabs/cli-agent-orchestrator issue #182 records it: "Kiro CLI 2.0 TUI redraws the screen in-place… retains 'Kiro is working' from earlier rendering alongside the new idle prompt", with the stated impact "Handoff delegations never complete." Two projects independently landed on the same mitigation: anchor to the bottom N lines or to the OSC title, never to the whole screen, and add NOT-gates for stale artifacts.

Two disclosures on that citation, because they change how much weight it carries. First, issue #182 is closed as completed on 2026-04-20 and its title begins fix(kiro_cli): — the bug was fixed. It remains valid evidence that the failure mode is real and was hit in production by a shipped project; it is not evidence that any current tool is broken today. Second, awslabs/cli-agent-orchestrator (the AWS project cited here) and Untrivial-ai/agent-orchestrator (the home of ao pty-host, discussed elsewhere in this guide) are different, unrelated projects that happen to share a name.

The judgement, with the headless constraint applied: scraping is disqualified as an architecture — not because any specific instance is broken today, but because a stale-buffer misread has nobody to notice it when no human is in the loop. Between the remaining two poles, vendor-emitted structured events cost nothing structurally — they do not touch the PTY at all, so they survive an ADE swap for free — which makes them the cheapest channel to add and a reasonable default if you are willing to write and maintain a separate integration per agent vendor. Native typed state costs you a VT emulator's fidelity and buys you one channel that works for every agent uniformly. That is the actual trade, and which side of it you want depends on how many agent CLIs you intend to support.

One number to state precisely, because it is quoted wrongly. The 34 claude_code.* identifiers named in the opening section are not 34 events: the set mixes metrics (cost.usage, token.usage, session.count, lines_of_code.count), events, and spans (tool.execution, hook). Say "34 named claude_code.* identifiers".

Substrate stance — wrap tmux, replace it, or skip the PTY

Pole How to determine it Where the field lands
Wrap tmux, keep it as substrate tmux appears as a hard prerequisite in the install docs, or as a spawned binary in source tmuxai, tmux-agent-status, tmux-message-bus, claude-squad, uzi, Agent Orchestrator on Darwin and Linux
Replace tmux with a new PTY-owning binary A PTY library appears in the dependency manifest and no tmux prerequisite exists psmux, quil, oly, qscreen, Zellij, rmux, herdr, boo
No multiplexer at all, no PTY No PTY library anywhere; the agent is spawned with ordinary pipes Claude Code -p --input-format stream-json; Codex app-server; OpenHands EventStream; the GitHub Actions runner

On native Windows the first pole is a disqualifier disguised as an architecture choice, which is why it is worth checking early and cheaply: a tmux prerequisite anywhere in the install path means WSL.

The criterion that decides between the second and third poles, stated independently of any one project: a session layer is necessary exactly when the agent host does not own process lifecycle end to end. If the agent tool spawns, supervises and outlives its own processes, a multiplexer underneath is dead weight. If the host dies and takes its children with it, the multiplexer is the only thing that makes sessions survivable.

That line is not this guide's invention — an independent practitioner draws the same one. Galen Guan writes: "For Claude Code and OpenClaw users working through SSH-disconnected sessions, tmux is essential infrastructure. For Hermes users, it's an unnecessary abstraction layer" — Hermes being an agent tool whose own terminal tool owns process lifecycle end to end, and OpenClaw one that, like Claude Code, does not. Treat that as corroboration, not proof: it is a third-party blog writing about somebody else's tools, not any project's own documentation.

Applied to your situation: ORCA spawns agent CLIs through its own in-process node-pty, so sessions die with ORCA. It is on the "essential infrastructure" side of the line by construction. The wrapper is not invented-here syndrome; it is the consequence of where ORCA puts the PTY.

Transport binding — four poles, and why "it uses a Unix socket" tells you nothing

The original binary formulation ("POSIX-only versus native-Windows") misclassifies candidates outright. AF_UNIX is a real Windows transport since Windows 10 1803, so a UnixListener in source is no longer proof of a POSIX-only daemon. This is the single most important correction in the taxonomy and the subsection below returns to it.

Pole The grep that proves it Where the field lands
cfg(unix)-gated UDS or forkpty std::os::unix::net, std.posix, forkpty, or nix with terminal features dtach, abduco, boo (std.posix in 11 files), ht (src/pty.rs, forkpty plus nix), tuios, gotty — whose creack/pty start_windows.go is literally return nil, ErrUnsupported
Cross-platform AF_UNIX shim UDS routed through a shim crate WezTerm (wezterm-uds/src/lib.rs: #[cfg(windows)] use uds_windows::UnixStream), Codex (codex-rs/uds/src/lib.rs:1 "Cross-platform async Unix domain socket helpers"; line 162 #[cfg(windows)] dispatches to uds_windows::UnixListener::bind)
Windows named pipes \\.\pipe\, CreateNamedPipeW, interprocess::GenericNamespaced, tokio::net::windows::named_pipe oly, qscreen, Zellij, rmux, herdr, node-pty (conin/conout)
TCP loopback TcpListener::bind("127.0.0.1:0") psmux (control and cross-session), ao pty-host, Warp local_control (http://127.0.0.1:PORT/v1/control), pywinpty's internal bridge
Naked AF_UNIX on all platforms net.Listen("unix", …), unconditional quil (internal/ipc/server.go:311) — works on Windows 10 17063+, but os.Chmod(path, 0600) is a no-op on Windows and there is no SO_PEERCRED, so the socket's only protection is the parent directory's ACL

Run all five greps at once from the repo root:

# Ripgrep, single-quoted so PowerShell passes the regex through untouched.
# (Select-String -Pattern takes the same patterns if you would rather not install rg.)
rg -n 'std::os::unix::net|std\.posix|forkpty|net\.Listen\("unix"'   # POSIX-shaped
rg -n 'uds_windows|interprocess::|GenericNamespaced'                # shimmed, or named-pipe crate
rg -n 'CreateNamedPipeW|named_pipe|pipe\\'                          # named pipes
rg -n 'TcpListener::bind|net\.Listen\("tcp"'                        # loopback TCP
rg -n 'FILE_FLAG_FIRST_PIPE_INSTANCE|SECURITY_ATTRIBUTES|security_descriptor|ImpersonateNamedPipeClient'   # does it defend the endpoint?

That last line is the one people skip. The two AF_UNIX poles differ in hygiene, not in portability — both work on Windows 10 17063+. The difference is that a shim crate documents the platform intent and centralises the fallbacks, whereas a naked net.Listen("unix", …) leaves the caller believing POSIX socket semantics still apply. Neither pole gets kernel-enforced permissions on Windows: SO_PEERCRED does not exist there, and chmod or os.Chmod on an AF_UNIX path is a no-op. Whichever pole you are in, an AF_UNIX socket on Windows is protected only by the ACL on its parent directory. A properly ACL'd named pipe is strictly stronger; a loopback TCP port is strictly weaker — any local process can scan it.

Two crates are the de-facto answers, arrived at independently. WezTerm and Codex both landed on uds_windows; Zellij, herdr and oly all landed on interprocess. WezTerm's own docs/multiplexing.md states the consequence flatly: "Unix domains are supported on all systems, even Windows."

Persistence scope: what exactly survives what

How to determine the level, and this one is an experiment rather than a grep: kill the spawning process, then ask two questions — is the child still running, and can a new client re-attach to it? Both yes is level 2. Child alive but no re-attach path means the tool is a supervisor, not a session host. Neither is level 0.

Level Meaning Where the field lands
L0 — none A new process per connection, killed on disconnect ttyd (src/protocol.c:377-383LWS_CALLBACK_CLOSED unconditionally kills the per-connection pss->process), wetty (src/server/spawn.ts, .on('disconnect', () => term.kill())), terminado UniqueTermManager
L1 — survives client disconnect The session lives as long as the host process upterm, sshx, terminado NamedTermManager, ACP — the Agent Client Protocol, covered among the interface shapes below — whose session/load restores conversation state, not a process
L1+ — L1 plus cursor-resumable replay Survives client disconnect and a reconnecting client resumes from an absolute output cursor rather than replaying from zero. Still dies with the host process, so it is not L2 OpenCode /pty (packages/core/src/pty/protocol.ts) — the only instance found anywhere
L2 — survives the ADE A detached daemon outlives whoever spawned it psmux, quil, oly, qscreen, Zellij (zellij-client/src/lib.rs:463-480, CREATE_NO_WINDOW | CREATE_NEW_PROCESS_GROUP), rmux, ao pty-host (conpty/spawn_windows.goCREATE_NEW_PROCESS_GROUP + DETACHED_PROCESS "so the host survives daemon exit"), wezterm-mux-server --daemonize
L3 — survives a host reboot State is restored across a full restart cmux (manaflow-ai/cmux, Swift + AppKit, GPL-3.0-or-later, 25,483 stars, macOS only), whose FAQ claims "the state survives a full computer restart… Agent sessions like Claude Code, Codex, and OpenCode come back too" — the only level-3 claim found anywhere in this research, and one you cannot check on Windows because the product does not run there. It is quoted from cmux's own FAQ, which was read once and for which no URL was recorded; treat the claim as reported rather than verified

L2 is the level that defines the thing you are shopping for. It is also the level a README will claim on the strength of L1 behaviour, which is why the check is a kill command and not a document. Substitute your candidate's binary name and verbs into $tool below — the block is written with variables rather than <placeholders> because in PowerShell < is a reserved redirection operator and a bare <tool> is a parse error, not a blank:

# 0. Name the thing under test once.
$tool = 'psmux'          # or zellij / rmux / herdr …

# 1. Start the daemon the way the ADE would: from a shell you are about to destroy.
#    Start-Process gives you a genuinely separate console host, so killing it later
#    does not take down the terminal you are typing in.
$parent = Start-Process pwsh -PassThru -ArgumentList '-NoExit','-Command',"$tool new -s probe"

# 2. Record the daemon's PID *before* you kill anything — it is the thing under test.
$daemon = Get-Process | Where-Object { $_.ProcessName -like "*$tool*" } |
          Select-Object Id, ProcessName, StartTime
$daemon
$daemonPid = $daemon[0].Id

# 3. Kill the spawning shell the way a crash would. NOT `taskkill /T` — see below.
Stop-Process -Id $parent.Id -Force

# 4. From a brand-new shell: is the daemon alive, and can a NEW client take the session over?
Get-Process -Id $daemonPid -ErrorAction SilentlyContinue   # alive?  -> at least L1
& $tool ls                                                 # visible to a new client?
& $tool attach -t probe                                    # re-attachable?  -> L2

Steps 2 and 4 are the honest version of the test: a tool passes L2 only when a different client process, started after the original died, can drive the session. Do not reach for taskkill /T — it walks ParentProcessId and kills a correctly-detached daemon anyway, so it measures the process tree rather than the design; the full reasoning, and the Job-Object variant that is worth testing separately and knowingly, are in The verification ritual. These verbs are assembled from each project's own documentation and verb list and have not been executed on a Windows machine in any pass, so treat the exact spellings as a starting point and the shape of the test as the thing to preserve.

Ownership timing: settled, and recorded so you do not re-open it

This is not really an axis, because one pole is refuted outright and there is nothing left to place. It keeps its slot because it is the question that gets re-opened every time somebody re-researches this area.

Own-from-birth — the wrapper spawns the child and holds the HPCON from the first instruction — is the only viable pole, and every working implementation found does it. Late attach, reaching into a console the wrapper did not spawn via AttachConsole(pid), is a read-only escape hatch at best. The evidence is under AttachConsole and Who owns the session above; the short version is that node-pty, the one piece of prior art everybody cites, hands AttachConsole the PID of a process it spawned itself (src/windowsPtyAgent.ts:149), so it proves the opposite of what it is cited for. Nothing found anywhere late-attaches to a foreign console and drives it, with the single exception of one hobby project.

The check, on a repository you have never opened: grep for AttachConsole and CreatePseudoConsole, then read whose PID goes into the attach call. If it is the tool's own child, it is own-from-birth whatever the README implies.

The placement matrix — every live candidate on all ten axes

Rows are the candidates that survived the census. Read a column to compare one design decision across the field; read a row to characterise one tool. Cells are pole names, abbreviated; means the axis does not apply to that candidate's shape.

Candidate Coupling State ownership Namespace Command lang Extensibility Agent state Substrate Transport Persist Timing
psmux Monolithic Full VT (TUI) / raw-by-intent (-CC) — the %output ring is octal-escaped but drained through String::from_utf8_lossy first, so on the strict payload-type test it is full VT Socket-name (-L/-S) One grammar Wire protocol (-CC) Native typed Replace TCP loopback L2 Own-from-birth
Zellij Monolithic Full VT owner Socket-name Hint-first WASM plugins Native typed Replace Named pipes L2 Own-from-birth
rmux Monolithic Full VT owner Identity-scoped One grammar Typed SDK Native typed Replace Named pipes L2 Own-from-birth
herdr Monolithic Full VT owner Socket-name One grammar Typed SDK (3rd-party) Native typed Replace Named pipes L2 (beta) Own-from-birth
oly Monolithic Parallel/raw Socket-name (OLY_SOCKET_NAME), machine-global default One grammar HTTP/WS Native typed Replace Named pipes L2 Own-from-birth
qscreen Session-only Parallel/raw Single fixed (per-user) One grammar Wire protocol Native typed Replace Named pipes L2 Own-from-birth
quil Monolithic Parallel/raw Env-var home (QUIL_HOME) One grammar MCP (18 tools) Native typed Replace Naked AF_UNIX L2 Own-from-birth
wezterm-mux-server Monolithic Full VT owner Socket-name (domains) One grammar Lua + wire codec Native typed Replace AF_UNIX shim L2 Own-from-birth
OpenCode /pty Session-only Full VT (UTF-8 string wire) — (HTTP, per-server) HTTP/WS Vendor events No multiplexer HTTP/WS L1+ Own-from-birth
ao pty-host Session-only Parallel/raw Registry file per session Binary wire protocol Vendor events Replace TCP loopback L2 Own-from-birth
upterm Session-only Opaque relay (SSH) — (SSH keys) One grammar Replace SSH L1 Own-from-birth
No-PTY architecture — (no VT at all) Env-var home (CLAUDE_CONFIG_DIR) CLI + NDJSON Hooks (http / mcp_tool) Vendor events No multiplexer pipes / HTTP you own it

Two things to notice in the columns rather than the rows. The persistence column is nearly uniform — almost everything that survives to this table is L2, which means persistence is table stakes and not a differentiator. The namespace and transport columns are where the field actually disagrees, and they are the two columns with security consequences.

Worked example — placing a tool that isn't in the census

The procedure is mechanical, and the fastest way to show that is to run it on something the census dismissed in one line. Here is mobydeck/atch — a C project, 318 stars, pushed 2026-03-20, whose own description is "atch lets you attach and detach terminal sessions" — walked through all ten axes from scratch. Note the language, because the checks have to be translated: the greps in this section are written in Rust idiom (crate, manifest, cfg(unix)) and the equivalents in a C tree are a different set of strings — look for libvterm/vtparse/terminfo where you would look for a VT crate, for the Makefile/configure.ac/#include <termios.h> where you would read Cargo.toml, and for #ifdef _WIN32 where you would read #[cfg(unix)]. The axis does not change; the string you grep for does.

  1. Coupling — no split / new-window / select-pane verb in the CLI, so session-only.
  2. State ownership — no VT or terminal-emulation dependency anywhere in the build (no libvterm, no vendored parser, nothing in the Makefile), and it forwards the child's stream, so opaque relay. No redraw on reattach.
  3. Namespace — the socket path comes from an argument, so socket-name-as-namespace.
  4. Command language — one small verb set, no config-file grammar, so one grammar, trivially.
  5. Extensibility — none published, so no pole.
  6. Agent state — nothing typed exposed, so no pole; a consumer would have to scrape, which the headless constraint disqualifies.
  7. Substrate — owns its own PTY, no tmux prerequisite, so replace.
  8. Transport — POSIX sockets and forkpty throughout, with no #ifdef _WIN32 branch and no Windows build path at all, so POSIX-gated UDS. This is the disqualifying cell.
  9. Persistence — a detached daemon, re-attachable, so L2.
  10. Timing — spawns its own child, so own-from-birth.

Refuted, on the native-Windows constraint, decided entirely by axis 8. Two properties of that outcome are worth naming, because they are what you get from running the procedure rather than forming an impression. The conclusion falls out of a single cell — you did not have to weigh L2 persistence against the missing plugin system, because a POSIX-only transport ends the conversation. And the reason is recorded in a form the next reader can re-check in one grep, which is what stops the same tool being re-evaluated in six months. Every refuted candidate in the census was reached this way.

The sequence in commands, against a repo you just cloned:

# Variables, not <angle brackets>: `<` is a reserved redirection operator in PowerShell.
$repo = 'https://github.com/mobydeck/atch'
$tool = 'atch'
git clone $repo tool
Set-Location tool

& $tool --help                                          # axes 1 and 4: which verbs exist?
rg -n 'vt100|vte|termwiz|ghostty|charmbracelet/x/vt|libvterm'   # axes 2 and 6: an emulator at all?
rg -n -g '*protocol*' 'Data|payload|chunk'              # axis 2: bytes or string on the live path?
rg -n 'socket|pipe_name|SOCKET_NAME|_HOME'              # axis 3: what is the default name made of?
rg -n 'tmux'                                            # axis 7: is tmux a prerequisite?
# axis 8: the five transport greps above
# axis 9: the kill-the-parent ritual above
rg -n 'AttachConsole|CreatePseudoConsole|forkpty'       # axis 10: whose PID goes into the attach call?

A trap in step two, and it is an easy one to fall into: a file sitting in a tests/ directory is not a test. oly's tests/output-copilot.log reads as evidence of live agent-CLI end-to-end tests; it is a 17,718-byte recorded ANSI dump consumed by src/session/logs.rs, the log-rendering unit tests, and by nothing that attaches to a live agent. Before citing a test artifact, find the code that opens it.

Seven traps that will misclassify a tool

Each of the following is a mistake a competent engineer makes independently, and most of them quietly disqualify a viable tool or promote a broken one. They are stated as standing traps because they will still be traps when this census is stale.

Trap 1 — reading "it uses a Unix socket" as "it cannot run on Windows". The intuition is that a UnixListener, a net.Listen("unix", …), or an #[cfg(unix)] module means the tool is POSIX-only. That has been false since Windows 10 1803, when AF_UNIX became a real Windows transport; the sockets work on Windows 10 17063 and later. WezTerm proves it deliberately (wezterm-uds/src/lib.rs dispatches to uds_windows::UnixStream under #[cfg(windows)]) and Codex proves it deliberately (codex-rs/uds/src/lib.rs:1 opens with "Cross-platform async Unix domain socket helpers"). quil proves it accidentally, with an unconditional net.Listen("unix", …) at internal/ipc/server.go:311 that works on Windows anyway. So quil is simultaneously an AF_UNIX tool and a viable native-Windows candidate; both are true. Keep the hygiene question ("does the code know it is on Windows?") separate from the portability question ("does it run there?") — that is why the transport axis has four poles instead of two. Two related beliefs die with this trap: quil's own ADR-2 says it uses named pipes on Windows, which was never implemented (no build-tag split in internal/ipc/, no go-winio in go.mod, the claim exists only in prose at docs/architecture.md:20); and psmux was believed to have migrated to named pipes via PR #13, which was closed without being merged, leaving psmux with no named-pipe IPC at all and a bolted-on AUTH key over TCP.

Trap 2 — treating state ownership as a binary, and concluding the double parse is unavoidable. It is not. AttachMode::Bytes in qscreen, the WebSocket Data message in oly, MsgPaneOutput in quil and %output in psmux's -CC mode all ship bytes on the live path today while maintaining a VT model for snapshots. Two pieces of evidence commonly cited for the old binary are misreadings. ripple's HANDOFF_GARBLING.md shows the opposite of what it is cited for (yotsuda/ripple, a C# MCP-plus-ConPTY session tool): the broken component was CommandOutputRenderer, a naive logical-line model with MaxCol=100,000 and no grid, which interpreted \r\n as a real logical newline and let a following cursor-position sequence collide with a prompt echo already sitting on that row. The VT-emulating path — the one the double-parse story blames — is the path the document says was fine throughout: its peek_console route goes through VtLiteState, which maintains a visual grid, "so it was OK from the start". Two disclosures on that quotation. The document is written in Japanese, so the English wording here is a translation, not a verbatim quote — do not re-quote it as one. And no repository URL was recorded for the file, so treat the pointer as approximate; the finding, that a grid-based model was not the broken component, is the load-bearing part and it is legible in the source either way. And quil's 0x9C-in-UTF-8 bug is not double-parse corruption: quil's own tech-debt notes classify it as an upstream charmbracelet/x/vt spec-compliance defect with a known correct general fix, already worked around, and found on macOS.

Trap 3 — treating persistence as a boolean. "Does it persist?" collapses at least four distinct behaviours, and the collapse always flatters the tool. ttyd is widely recorded as having "partial persistence" and has nonesrc/protocol.c kills the per-connection process unconditionally on close. Coder's reconnectingpty reconnects, which reads as persistence, but self-terminates after five minutes with nothing attached. OpenCode's /pty needed a level of its own (L1+) because cursor-resumable replay genuinely is more than L1 and genuinely is not L2. And the blanket claim that every non-multiplexer route gives up persistence or reattach is false in both directions: Windows' own tscon gives you both natively, and codex app-server --listen ws:// is a standing listener the client does not own.

Trap 4 — believing late attach is a live design pole. node-pty passes its own child's PID to AttachConsole, so the prior art everybody cites proves the opposite of what it is cited for. Covered in full above.

Trap 5 — checking the header instead of the consumer, and the constant instead of the default. A #define, a literal, or a flag in a header tells you what a value can be, never what the running code does with it. This trap fires twice in this guide with opposite signs: the "portable-pty confers a flag advantage over node-pty" claim survived for years on two header reads and dies on one code search over microsoft/terminal for who actually reads dwFlags; and oly gets placed at the bottom of the namespace axis by anyone who reads its default socket-name literal without reading the std::env::var call two lines up.

Trap 6 — treating a file in tests/ as a test. oly's tests/output-copilot.log reads as evidence of live agent-CLI end-to-end tests. It is a 17,718-byte recorded ANSI dump consumed by src/session/logs.rs, the log-rendering unit tests, and by nothing that attaches to a live agent. Before citing a test artifact, find the code that opens it.

Trap 7 — treating "a Windows release artifact exists" as proof of a working Windows daemon. ttyd is the counterexample and it is exact: real ConPTY-capable source, an MSVC fix merged 2026-03-19, and the only shipped Windows binary — ttyd.win32.exe, MinGW, 1.7.7, 2024-03-30 — cannot spawn a child on Windows 11 build 26200. Check that the released artifact is recent enough to contain the fixes in master, and check what your package manager actually pins; WinGet and Scoop both still serve the broken 1.7.7.

And one more that is about metadata rather than design, because it flipped a verdict here. updated_at on a GitHub repository ticks on stars and watches. Read pushed_at. A project can look updated three weeks ago and have had no code pushed for nineteen months.

The seven standalone daemons

Ten projects on native Windows satisfy the structural predicate — they own a ConPTY, they survive the client that spawned them dying, and they expose an interface that is not a GUI. Read "the client" precisely: it is what lets OpenCode's /pty into the set at L1+ even though its sessions die with the opencode serve host process. Surviving the host is L2, and that is a stronger property that most but not all of the ten have. Seven of them are standalone daemons you can install or build and point an ADE at today. Three more exist only inside a larger product and are covered after these.

Every one of them is own-from-birth: the daemon spawns the agent and holds the HPCON from the first instruction, for the reasons set out under Who owns the session. None reaches into a console it did not create.

Star counts and last-activity dates below are a 2026-08-02 snapshot and churn weekly. Read them as a rough liveness signal, not a number.

Before the individual entries, the one procedure that decides the category. Every daemon here claims to survive its parent. Start a detached session, kill the parent terminal window outright, open a new terminal and run the tool's ls verb: still listed and re-attachable is level 2, and level 2 is the whole point. A tool printing "session created" proves nothing about it, and neither does a daemon still visible in Task Manager — a live child with no re-attach path is a supervisor, not a middle layer, and both questions have to answer yes. The exact commands, including the kill that measures the design rather than the process tree, are in The verification ritual.

A caveat that applies to every command block from here to the end of the guide. The install and drive lines come from each project's own install documentation and verb list. None of them was executed on a Windows machine in any pass of this research. Treat them as the right shape and verify the exact spelling against the version you download.


psmux — a Windows-only tmux workalike with tmux's control mode

What it is. A Rust tmux workalike, MIT, 3,140 stars, pushed 2026-08-02, that speaks the tmux verb set and tmux's machine protocol — and runs on Windows and nowhere else.

How it works. psmux owns the ConPTY through a vendored portable-pty-psmux 0.9.6 at crates/portable-pty-psmux, patched in-tree. Its control listener is loopback TCP — TcpListener::bind(("127.0.0.1",0)) at src/server/mod.rs:839 — with an application-layer AUTH <key> handshake at src/server/connection.rs:304-315. A client speaks either ordinary tmux CLI verbs or tmux -C / -CC control mode: you send tmux commands, it replies with %begin / %end / %output / %layout-change lines, and the outer program owns rendering while psmux owns sessions. The control-mode implementation is src/control.rs (554 lines) plus docs/control-mode.md (432 lines) — this is a documented surface, not an accident. Sessions are level 2: they survive the parent.

Note what the two modes do to your bytes. The TUI path is a full VT owner — it parses everything into a grid and re-emits rendered ANSI. The -CC path ships the pane's output ring octal-escaped through %output (src/server/mod.rs:1222-1227), which keeps the ADE's own xterm.js closer to being the only renderer in the chain.

Install and drive it.

winget install psmux.psmux        # or: scoop install psmux
psmux new-session -d -s probe     # detached session
psmux ls                          # then kill the parent window and run this again

What it is genuinely good at. Two things nothing else in the census matches. First, the contract is tmux's, which is the most-cloned terminal contract in existence — at least five independent implementers of -CC exist (iTerm2, WezTerm, psmux, Tomiyou/ivyterm, paulrobello/par-term), so a client you write against it is not client to one project's whims. Second, its CI runs cargo audit --deny warnings against both the workspace and the test-monitor lockfile, a supply-chain gate no other small candidate has. It also answers ConPTY's startup handshake, with a dedicated tests-rs/test_cpr_responder.rs, and its pipe mode negotiates terminal size in band rather than guessing — the mechanism, and the 120x30 fallback that makes it a hazard if nothing answers, are under the four things a middle layer must get right on Windows.

And psmux carries the only external field report in this entire research, which is worth more than its tier suggests. Almost nothing in this guide has been run by anybody other than a project's own author. The one exception is a multi-week writeup by an independent developer running psmux with Copilot CLI across git worktrees on native Windows: "I can detach from a session, close my terminal, come back hours later, and reattach to find everything exactly where I left it — agents still running, output still visible… No WSL required. No dependencies." That is a personal blog, not a project's own documentation, and it should be read at that tier — but it is the strongest available evidence that somebody other than the author runs any candidate in this guide in anger, and that L2 persistence works in real use on native Windows, which is the exact property you are shopping for. (One correction on provenance: this report has circulated with its author described as "a .NET MVP". The string "MVP" does not appear on the cited page and that descriptor is withdrawn.) Where this guide says "almost nothing here has been run", this is the qualification.

The unauthenticated cross-session port — output exfiltration and input injection through one socket. This is the defect to read twice. Alongside the authenticated control listener, src/cross_session_server.rs opens a second, completely unauthenticated channel. Line 98 binds TcpListener::bind("127.0.0.1:0"). Line 127 accepts the first connection with no handshake at all. Lines 131-132 push that socket into crate::types::PIPE_WRITERS as a tee writer, so the connecting process receives a copy of every byte the pane's ConPTY emits. Line 149 writes everything it reads back into pty_writer.write_all(). That is full-duplex — exfiltration and injection — unauthenticated, on a loopback port any local process can scan, gated only on the user performing a cross-session pane move. Whether that gate is reachable in normal operation has not been traced (Q6, about three hours: trace every call site of cross_session_server, or attempt a proof-of-concept connect during a pane move). Until someone does, treat psmux as unsafe on a shared or multi-user machine, and pin the version you audit.

The from_utf8_lossy that corrupts Unicode permanently. src/server/mod.rs:1225 calls String::from_utf8_lossy(&bytes) on each ring drain before escaping, so a multi-byte character split across two drains becomes U+FFFD and never recovers — the replacement character is what gets stored and re-emitted, not merely what gets displayed. This is a bytes-to-string bug rather than a double-parse bug, and it is a property of string-typed terminal transports generally rather than one project's defect; the general case, and OpenCode's instance of it, are under Nesting.

Two smaller things that will bite you. The control-mode auth key is generated from std::collections::hash_map::RandomState over a nanosecond timestamp plus PID, truncated to 64 bits (src/server/mod.rs:852-860) — explicitly non-cryptographic per Rust's own documentation — and the key file gets no explicit ACL, with the source comment at :864 conceding that "user-only visibility on Windows comes from the profile directory ACLs". And the nesting guard names the wrong environment variable in its own error message: the check at :954 and :4056 reads PSMUX_ALLOW_NESTING, while the text printed at :958/:4060 says "psmux: sessions should be nested with care, unset PSMUX_SESSION to force". A scripted wrapper that obeys the message will not clear the guard.

Orphaned processes on Windows teardown — an operational defect, unevenly distributed. A blanket claim that all four small candidates leak orphaned processes when a session is torn down on Windows does not survive checking, but what does survive is uneven and worth knowing before you run any of them at one-agent-per-worktree scale: issue-tracker evidence supports it for psmux, partially for quil, shows zero evidence for oly, and is necessarily zero for qscreen — which has no external users to file reports, so its silence means nothing either way. Treat orphan cleanup as something you verify yourself (start ten sessions, kill the daemon, count what is left in Task Manager) rather than something any of these projects has demonstrated.

And the platform, stated plainly. psmux is Windows-only. Its CI builds only x86_64/i686/aarch64-pc-windows-msvc; release v3.3.7 ships six Windows-only assets; the ubuntu-latest/macos-latest job is called posix-helper-tests and runs two shell scripts, not a build. Language statistics that say "mostly PowerShell" are misleading — the installer and test suite are the byte count, the product is the Rust binary.

One more, if you plan to use -CC from inside an ADE. The maintainer's own warning at src/main.rs:4188-4193 scopes ConPTY's DCS-eating corruption to the SSH case, and whether it generalises to a node-pty ConPTY is an inference rather than psmux's claim — the quote, the corroborating Microsoft issue and the remedy are under Nesting. The order to test in matters more than the test: ask whether ORCA's agentCmdOverrides can produce a raw-pipe child at all (Q1a) before measuring whether the corruption happens (Q1), because if the cure is unreachable the disease is moot and psmux drops to its plain CLI verbs.


Zellij — best-verified Windows claim and an explicit nesting policy, at the cost of an unverifiable detached-start path

What it is. A Rust multiplexer, MIT, 34,647 stars, pushed 2026-08-01, whose Windows port shipped in 0.44.0 — and the candidate whose Windows support claim is best verified in this census, from source through to release artifacts. Not the only one so verified: rmux is source-verified (stream_windows.rs, server_identity_windows.rs, rmux-pty/.../io.rs) and ships winget, scoop and choco packages, and psmux is source-verified with six Windows assets on v3.3.7. Zellij's advantage over both is breadth, not uniqueness.

How it works. ConPTY directly: zellij-server/src/os_input_output_windows.rs calls CreatePseudoConsole / ResizePseudoConsole and holds the HPCON. IPC is Windows named pipes through the interprocess crate (zellij-utils/src/ipc.rs:8). The client is the zellij CLI plus a WASM plugin runtime that has been protobuf-schema'd since 0.38.0. Zellij is a full VT owner — it parses into a grid and re-emits. Resize is handled by polling crossterm::terminal::size() every 50 ms on the VT-reader path (os_input_output_windows.rs:95 doc-comment, :176 sleep), with a second 100 ms poller at :38/:52.

Persistence is level 2. Be careful about what the usual citation for that proves, because the source says something different from what it is quoted for. zellij-client/src/lib.rs:463-480 spawns with CREATE_NO_WINDOW | CREATE_NEW_PROCESS_GROUP, and those flags are frequently quoted as "the mechanism" for surviving the parent. They are not. CREATE_NO_WINDOW suppresses a console window; CREATE_NEW_PROCESS_GROUP detaches Ctrl+C group delivery; neither implements parent-death survival, and on Windows a child does not die with its parent by default anyway — which is what actually makes L2 nearly free here. The doc-comment above that spawn says so in as many words, and it is worth reading because it also warns you off the flag you might otherwise reach for:

On Windows there is no daemonize — we launch the server as a background process with a hidden console. We use CREATE_NO_WINDOW (not DETACHED_PROCESS) so the server gets valid standard handles; DETACHED_PROCESS leaves stdin/stdout/stderr as NULL, which breaks PTY creation, WASM plugin loading, and logging.

So Zellij's L2 is real, the flags are hygiene rather than the load-bearing part, and there is a design lesson in there for anyone copying ao pty-host's CREATE_NEW_PROCESS_GROUP | DETACHED_PROCESS pattern: DETACHED_PROCESS nulls your standard handles, and Zellij hit that and backed out of it.

Install and drive it.

winget install zellij-org.zellij   # or the v0.44.3 x86_64-pc-windows-msvc .msi / .zip
zellij list-sessions

One honest gap in the drive line, and it is in the verb you need most. Zellij has no documented flag equivalent to psmux new-session -d — no -d, no --detached. The invocation zellij --session probe options --help that circulates as a "create a session" line does nothing of the sort: it prints help and creates no session. This guide has no verified detached-start invocation for Zellij, and that is a real hole, because step 1 of the verification ritual cannot be run against Zellij with anything here. The interim path is the interactive one — zellij attach --create probe to make or join a session named probe, then detach with the Ctrl-o d chord — which works but needs a human at the keyboard, which is exactly the property under test. Settle it by reading zellij --help and zellij options --help on the version you install, and do that before you conclude anything from a failed step 1.

What it is genuinely good at. Maturity and packaging: v0.44.3 ships zellij-x86_64-pc-windows-msvc.zip and an MSI, and Linux and macOS were the original targets, so you get uniform behaviour across the platforms you run. It is also the only candidate with an explicit nesting policy: nested_session_handling takes ask / fullscreen / descend / never at zellij-utils/assets/config/default.kdl:268-275. Every other multiplexer either refuses nesting or lets you force it with an environment variable; Zellij lets you declare what should happen.

The TERM/WT_SESSION gate — do this first, then read why. Set TERM in your override command and compare:

$env:TERM = 'xterm-256color'   # forces the VT input path; re-run and compare behaviour

Now the reason. zellij-client/src/os_input_output_windows.rs:32 decides between a VT-byte input path and a native-console INPUT_RECORD path with use_vt_path() { env::var("TERM").is_ok() || env::var("WT_SESSION").is_ok() }. Inside an ADE's node-pty ConPTY, neither variable is necessarily set, so Zellij takes the else branch. Be precise about what that means: the else branch is the native-console INPUT_RECORD path, it is implemented, it is the conservative choice under a real console, and ConPTY does give the child a real console. Calling it the wrong path is unevidenced. What is genuinely unknown is what the symptom would even be if the native-console path misbehaved under a nested ConPTY — dead keyboard, garbled input, or silently fine — so there is no pass/fail criterion until someone runs it. This is a configuration check, not a hazard; the test costs about an hour (Q3) and settles the objection most often raised against Zellij on Windows.

Known Windows rough edges, from the project's Windows tracking issue #4745 — opened by imsnif, the project lead, with divens (a contributor, and the author of the Windows port) as its heaviest commenter: no sixel, unreliable OSC 7 and live-cwd under PowerShell, focus-report sequences not reaching subpanes, and scroll-mode stickiness. (If you go looking for the config file, note the path is zellij-utils/assets/config/default.kdl — the bare assets/config/default.kdl path that circulates elsewhere 404s.)


rmux — identity-scoped named pipes and typed SDKs, against a single-author bus factor

What it is. A Rust daemon with 90-plus tmux-compatible verbs, 2,533 stars across 12 crates and 21 MB, pushed 2026-07-26.

How it works. Named pipes — but the pipe name is computed, not chosen. crates/rmux-ipc/src/endpoint.rs builds \\.\pipe\{prefix}-{sid}-il-{integrity}-{label} from the caller's SID and integrity level, so the namespace cannot be entered by a process that merely guesses a string. It is the only identity-scoped namespace found anywhere, and it is the pole every other candidate falls short of. On top of that it sets FILE_FLAG_FIRST_PIPE_INSTANCE (crates/rmux-pty/src/backend/windows/io.rs) so a hostile process cannot pre-create the pipe, and it validates the connected peer with ImpersonateNamedPipeClient (crates/rmux-ipc/src/stream_windows.rs). The PTY layer uses PROC_THREAD_ATTRIBUTE_PSEUDOCONSOLE and Job Objects with CREATE_BREAKAWAY_FROM_JOB, and has an explicit refusal path rather than a silent degrade — the error string is "breakaway process creation denied; refusing to run unguarded ConPTY child". That string is quoted from a source read once; its exact file and line were never recorded and could not be recovered, so take the string as reported and the behaviour as verified.

A client speaks tmux verbs, or typed SDKs in Rust, Python and TypeScript exposing session / pane / snapshot / wait_for_text. There is also a Ratatui widget and an end-to-end-encrypted web share. Persistence is level 2; the tool is a full VT owner.

Install and drive it.

winget install Helvesec.rmux      # or: scoop install rmux / choco install rmux
rmux new -d -s probe
rmux ls                           # after killing the parent window

What it is genuinely good at. Security posture and the agent-facing surface. wait_for_text and snapshot as first-class typed SDK calls are exactly the primitives a headless driver needs — you are not scraping a screen, you are asking a typed question. Combined with the identity-scoped namespace, it is the one candidate here whose security model has three independent layers rather than one, and if you are writing a middle layer yourself it is the shape worth copying — the namespace derivation in particular, which costs almost nothing to implement and removes a whole class of problem.

The failure here is procurement, not code. Two exposures, both human. 936 of roughly 1,000 commits come from a single author — a figure recorded once and never re-verified, so treat the exact ratio as approximate and the concentration as real. And the project is dual-licensed with the pair unstated in repository metadata; GitHub reports NOASSERTION. If you intend to vendor rmux, read the LICENSE-* files at the exact tag you would vendor before relying on it — you cannot answer the licensing question from the repo page. Nesting behaviour was never tested, on any pass.


herdr — a large project whose Windows build is on the preview channel

What it is. A Rust multiplexer/ADE hybrid, Apache-2.0, 23,466 stars, pushed 2026-08-01, with a CLI, an HTTP API (src/api/server.rs) and third-party TypeScript and Python SDKs.

How it works. Named pipes via the interprocess crate, same family as Zellij and oly. Full VT owner, level 2 persistence — but on Windows every one of those properties carries a beta label, from the project's own documentation.

Install and drive it.

# There is no winget/scoop path. Download the dated preview asset from the releases page:
#   https://github.com/herdrdev/herdr/releases
# The tag is a dated preview with a commit suffix - `preview-2026-07-29-44b3adb12552`, not
# the bare `preview-2026-07-29` that circulates - and the Windows asset in it is
# herdr-windows-x86_64.zip.  Confirm the exact tag on the releases page before scripting it:
$tag = 'preview-2026-07-29-44b3adb12552'
Invoke-WebRequest -Uri "https://github.com/herdrdev/herdr/releases/download/$tag/herdr-windows-x86_64.zip" `
                  -OutFile "$env:TEMP\herdr-windows-x86_64.zip"
# NOT stable v0.7.5 - that release ships linux and macos assets and NO Windows asset.
herdr list

What it is genuinely good at. It has the maintenance base — 23k stars and daily activity — that the small candidates lack, and an HTTP API plus SDKs means the drive surface does not depend on a terminal being attached. Its Windows-beta page lists "Nested launch override | beta" under Supported, which is as close as any project comes to documenting the topology you need.

"Direct terminal attach — unsupported": the reading first, then the evidence. The reading is that "Direct terminal attach" almost certainly means the Unix file-descriptor handoff path, not the ADE-spawn path — which would leave herdr a live candidate. That reading is an inference, not something herdr states, and it needs confirming: read src/api/, src/ipc.rs and the surrounding doc sections, and file one question on the repo if it stays ambiguous. Budget two hours (Q2). If it is the Unix mechanism, herdr is live; if it is the ADE path, herdr is out on Windows until the beta lands.

Here is what the inference rests on, and it is the shape of the page rather than any one row. The Windows-beta document (website/src/content/docs/windows-beta.mdx) has two tables, and reading only one of them is how this became the biggest open risk in earlier drafts of this research. The Supported table lists "Local persistent sessions | beta", "Native panes through ConPTY | beta", "Windows Terminal / PowerShell app attach | beta", "Pane screen history | beta" and "Nested launch override | beta". The Not supported table lists "Direct terminal attach | unsupported", "Live server handoff | unsupported", "Unix file-descriptor handoff | unsupported", "Unix foreground process groups | unsupported" and "Prefix input-source switching | unsupported". Read together: "Direct terminal attach" sits in a cluster of Unix-specific mechanisms — fd handoff, live server handoff, foreground process groups — while the ADE-spawn path is separately listed as supported in beta on the table above it. When a page is a capability matrix, the negative half is not the page.

The failure you will actually hit is the channel. There is no stable Windows release to pin. You install a dated preview build (preview-2026-07-29-44b3adb12552 at the time of writing — dated tag plus commit suffix), and the next preview replaces it. For a layer whose entire purpose is to be more durable than the ADE above it, depending on a preview channel is the wrong direction. Note also that "Prefix input-source switching | unsupported" is the only primary-source statement about prefix keys on Windows found anywhere in this research — every other candidate's rebindability is an inference from "it has a config file."


oly: real Windows PTY tests, no pipe ACL

What it is. slaveOftime/open-relay — a Rust session daemon, MIT (verified two ways: gh api reports spdx_id MIT and there is a LICENSE at the repo root), 89 stars, main HEAD 2026-07-01 with pushed_at 2026-07-30 across four side branches.

How it works. Named-pipe IPC through interprocess GenericNamespaced, plus an HTTP/WebSocket surface and push notifications. Its live path is raw bytesServerMessage::Data { data: Vec<u8> } at src/http/ws.rs:36-46 — so it maintains a VT model for snapshots and scrollback while shipping the child's actual bytes to the client. That matters: it means the ADE's own xterm.js can be the only VT parser in the chain, and the "unavoidable double parse" story is false. Level 2 persistence. The socket name comes from OLY_SOCKET_NAME and falls back to open-relay.oly.sock (src/config.rs:123-126).

oly is widely credited with answering ConPTY's startup handshake. Read the function before you rely on that, because it answers a different set of queries than the credit implies. extract_query_responses_no_client, in src/session/pty.rs, answers CPR, DSR and the OSC 10/11 colour probes when no client is attached, and its own comment states what it deliberately refuses: "Do NOT respond to: PrimaryDeviceAttributes (DA1), SecondaryDeviceAttributes (DA2), XtVersion (XTVERSION), DecPrivateModeReport (DECRPM), KittyKeyboard… Answering them in detached mode can cause interference with user input and corrupt the output stream." A unit test in the same file asserts that DA1, DA2 and XTVERSION go unanswered in detached mode. The DA1 reply string exists in the response generator (\x1b[?62;c) for the attached case; it is not emitted on the detached path. Since DA1 is the query behind the three-second startup stall, oly's detached mode does not eliminate that stall. Whether that costs you anything depends on whether something upstream answers DA1 — which is what the DA1 probe above tells you, and it is the first thing to check if oly-hosted spawns take seconds.

Install and drive it. There is no package-manager install line for oly — the winget/scoop/choco paths in this guide cover psmux, Zellij, rmux, herdr, wezterm-mux-server and OpenCode only. You clone and build, and the result is not on your PATH:

git clone https://github.com/slaveOftime/open-relay
Set-Location open-relay
cargo build --release              # generic Rust build; not a documented install path
# The binary lands in .\target\release\ and is NOT added to PATH. Invoke it by path,
# or add that directory to $env:PATH for the session:
$env:PATH = "$PWD\target\release;$env:PATH"
$env:OLY_SOCKET_NAME = 'oly-orca'  # per-instance namespace; the default has no user component

What it is genuinely good at. One thing, and it is not nothing: a real windows-latest PTY test matrix. tests/e2e_daemon.rs and tests/e2e_pty.rs run under a CI matrix that includes windows-latest (.github/workflows/ci.yml) and actually exercise ConPTY. No other small candidate has that. Add the raw-byte live path and MIT licensing and there is a real design here.

The unauthenticated named pipe with no ACL — the disqualifying-adjacent one. src/ipc.rs is 296 lines and contains no ACL, SID or security-descriptor keyword at all; FILE_FLAG_FIRST_PIPE_INSTANCE, security_descriptor and SECURITY_ATTRIBUTES are zero hits repo-wide. Microsoft's own CreateNamedPipe documentation states that a NULL security descriptor grants "read access to members of the Everyone group and the anonymous account." For a pipe carrying agent terminal I/O, that is an API-key disclosure path — whatever the agent prints, and whatever you type into it. And with no FILE_FLAG_FIRST_PIPE_INSTANCE, a hostile local process can pre-create the pipe and harvest client connections. The project knows: milestones/M4_security_hardening.md lists "Windows: enforce named-pipe ACL (current-user SID only)" with status "⏳ Not started", untouched since 2026-03-12. A later security audit (PR #83) fixed the web layer — 3 High findings including command injection in notification hooks, permissive CORS and no rate limiting on node-join, plus 14 Medium and 4 Low — and Unix socket modes, but no Windows pipe-ACL item appears in that findings table. Checking whether the gap was closed without updating the roadmap is a one-hour diff of src/ipc.rs across the commit history (Q13).

The nested-agent tests do not exist. This is worth stating because oly is the candidate most often credited with having them. tests/e2e_pty.rs is 895 lines and contains zero occurrences of copilot or opencode. It spawns shellscmd.exe, bash --noprofile --norc, pwsh. The tests/output-copilot.log file that makes it look otherwise is a 17,718-byte recorded ANSI dump consumed by src/session/logs.rs, the log-rendering unit tests — not by the PTY end-to-end tests, and not by anything that attaches to a live agent. oly tests shells under ConPTY on Windows. It does not test nesting and it does not test agent CLIs.

And a fixture that cuts against cross-platform uniformity. oly ships tests/output-copilot.expected.windows alongside tests/output-copilot.expected — a Windows-specific expected output for the same input log. That is direct evidence that its VT and log-rendering layer produces different output on Windows than on Unix. If you value one behaviour across both platforms, that fixture is the finding to weigh.

Two more sizing facts: the default socket name has no user component, so two users on one host collide unless one of them sets OLY_SOCKET_NAME (the name is env-selectable, which a one-file read misses — the isolation knob exists; the access control does not). And main has been still since 2026-07-01 while four side branches move, which is either a plateau or a departed maintainer; four to six weeks of watching issue-response latency settles it.


qscreen — anti-squatting and per-user namespacing, never run by anyone but its author

What it is. A Rust single-pane session daemon, MIT, 6 stars, pushed 2026-07-21 (the last default-branch commit is "docs(readme): add feature boundaries and quick start").

How it works. Named pipes — crates/qscreen-daemon/Cargo.toml carries the comment "Windows平台仅需tokio的named_pipe实现" ("on Windows only tokio's named_pipe implementation is needed"). The pipe is \\.\pipe\qscreen-<user> (crates/qscreen-shared/src/lib.rs:6-11), one instance per user by construction with no per-project knob, and the initial listener sets .first_pipe_instance(true) (crates/qscreen-daemon/src/lib.rs:162) so it cannot be squatted. The client is a qscn CLI speaking a structured ScreenFrame/ScreenRun protocol with FRAME_FLAG_*, FrameColor and FrameMouseModeand an AttachMode::Bytes raw-byte attach mode (crates/qscreen-protocol/src/lib.rs:56-61). Level 2 persistence. It is deliberately session-only: no split, no new-window, no select-pane verb exists.

Install and drive it. No packaged Windows install path was recorded. Build it:

git clone https://github.com/dualface/qscreen
Set-Location qscreen
cargo build --release              # generic Rust build; not a documented install path
# The client binary lands in .\target\release\ and is NOT on PATH:
.\target\release\qscn.exe --help   # verb spellings unverified - read them off the binary

What it is genuinely good at. Its protocol arrived independently at the same idea as tmux control mode — parse once, let the last hop render — and it offers both a structured frame path and a raw-byte path from the same daemon. Its two security choices are correct: anti-squatting on the listener and per-user pipe namespacing. It is not unique in that (rmux does both as well), but it is the sharp contrast with oly, which does neither.

Zero CI, zero issues, zero external users — nobody but the author has ever run it. contents/.github returns 404: there has never been CI on any platform. There are zero issues and zero pull requests in the project's history. This is not a claim that qscreen is broken — its design is sound and source-verified, and the honest reading is unvalidated on evidence, not refuted on design. But every property in the paragraphs above is "the source says so," with no second party ever having exercised it. There is one concrete aging tell to weigh with that: its portable-pty is 0.8, a full minor behind oly's 0.9 and psmux's patched 0.9.6, in a crate whose Windows backend changes between minors. (Its VT crate crates/vt100-psmux is a clean cargo vendor of the published vt100-psmux 0.16.2 — Jesse Luehrs' vt100-rust, patched and republished by marlocarlo — the same crate psmux vendors, arrived at independently rather than copied.)


quil — an 18-tool MCP surface, on ubuntu-only CI and a socket path that deletes live daemons

What it is. A Go session daemon, MIT (verified two ways), 11 stars, pushed 2026-08-01, shipping roughly one release a day, whose primary interface is an MCP server with 18 tools.

How it works. ConPTY through charmbracelet/x/conpty v0.2.0, with Microsoft's redistributable ConPTY bundled via scripts/fetch-conpty.sh and SHA256-pinned — and loaded by absolute path, filepath.Join(filepath.Dir(exe), "conpty.dll"), which is the safe way to do it. Live output is raw bytes: PaneOutputPayload { Data []byte } at internal/ipc/protocol.go:204-208. Screen state is typed: MsgScreenshotPaneResp{Text, CursorX, CursorY}. There is mouse-mode forwarding for nested alt-screen applications (internal/daemon/mousemode.go) — the only such accommodation this survey noticed, in an area it covered only glancingly, so read that as "nobody else advertises one" rather than as "nobody else has one". QUIL_HOME relocates the entire state root, which is the per-workspace isolation analogue of CLAUDE_CONFIG_DIR. Persistence is level 2. Clients: MCP, the quild CLI, and a TUI.

The transport is not what its own architecture decision record says it is. internal/ipc/server.go:311 is net.Listen("unix", …), unconditional on every platform. go.mod has no go-winio; the ipc/ directory has no _windows.go split. quil's own ADR-2 claims Named Pipes on Windows; that was never implemented. This is not a portability failure — AF_UNIX is a real Windows transport since Windows 10 1803, so the daemon does work — it is a security-semantics failure, covered below.

Install and drive it.

git clone https://github.com/artyomsv/quil
Set-Location quil
go build ./...                     # generic Go build; not a documented install path
# `go build ./...` writes the executables into the current directory and does NOT
# install them onto PATH (`go install ./...` would put them in $env:GOPATH\bin).
# Invoke quild by path, or add the build directory to PATH for the session:
$env:PATH = "$PWD;$env:PATH"
$env:QUIL_HOME = "$env:USERPROFILE\.quil-orca"   # isolate this instance - see the os.Remove problem

What it is genuinely good at. MCP is what an agent already speaks. Eighteen tools is the richest agent-facing surface in the census, and it means the middle layer's control plane needs no new client library — an agent can drive sessions with the same mechanism it uses for everything else. Add the raw-byte live path, typed screenshots and mouse-mode forwarding and the interface design is the best in the small-candidate field.

The unconditional os.Remove that bricks a live daemon. internal/ipc/server.go:309 is one line: os.Remove(s.path) // Clean up stale socket. It runs unconditionally at the top of Start(), before net.Listen, with no liveness check on the existing socket. A second quild started against the same QUIL_HOME therefore deletes a live daemon's endpoint by design. This is not a hypothetical: PR #51 documents a dated production incident on 2026-06-10 in which exactly that happened and "brick[ed] the original for new clients" — in the very instance-isolation mechanism that was previously reported as a strength. The same audit fixed a VT-emulator-disposal race that leaked a goroutine plus a 10,000-line scrollback grid per closed pane, an unbounded PTY-coalescer debounce buffer, and a Windows WaitExit handle leak per destroyed pane. If you run quil, give every instance its own QUIL_HOME and treat a second daemon on the same home as a destructive operation.

AF_UNIX on Windows with a no-op chmod. os.Chmod(path, 0600) does nothing on Windows, and there is no SO_PEERCRED, so the socket carries no kernel-enforced permission of any kind. Its only protection is the ACL on the parent directory. A properly-ACL'd named pipe — what the ADR promised — is strictly stronger; a loopback TCP port would be strictly weaker. The code sits in the middle and reads, to anyone who knows POSIX, as though the chmod did something.

And the validation gap. CI is ubuntu-latest only, across 25-plus Windows-specific source files. Zero external issues have ever been filed. Running its test suite on Windows — adding a windows-latest job locally and seeing what breaks — costs half a day (Q7) and is the difference between "richest agent-facing surface in the census" and "design reference". Separately: the in Claude Code's own window title already broke quil's emulator, which is the concrete instance of the Unicode failure class described under psmux.


Three that live inside a larger product

These three work, are source-verified, and are not shipped as the thing you want. You either run a host binary that a bigger product installs, or you vendor a package out of its tree. Licensing decides how comfortable the second option is.

wezterm-mux-server — a version-skew-tolerant codec, reachable on Windows only from a rolling nightly

What it is. The headless multiplexer inside WezTerm (28,101 stars, pushed 2026-07-31) — a separate executable, not a mode of the GUI. The RPM spec describes it in exactly those words: "Multiplexer server (headless)". On the licence, apply the same rule this guide applies to rmux, because the same signal is present: WezTerm is MIT in substance, but GitHub's licence API reports NOASSERTION for the repository (the file is LICENSE.md), which is exactly the condition that makes the rmux entry say "read LICENSE-* at the tag you would vendor". WezTerm's codec is the one thing in this guide the build-it-yourself path tells you to vendor, so read LICENSE.md at the commit you would vendor from rather than trusting a summary — including this one.

How it works. It daemonizes (--daemonize, handled explicitly on Windows) and owns the PTY. Transport is an AF_UNIX shimwezterm-uds/src/lib.rs does #[cfg(windows)] use uds_windows::UnixStream, and the project documents it: "Unix domains are supported on all systems, even Windows." The wire is a versioned binary codec with variable-length integer encoding of length, ident and serial (codec/src/lib.rs), and the reason is stated in the source: "so client and server can more gracefully manage unknown enum variants." That is the ADE-churn insulation you are shopping for, written down by someone who needed it for a different reason. It also ships a tmux -CC client (wezterm-escape-parser/src/tmux_cc/tmux.pest, mux/src/tmux.rs, tmux_pty.rs:90-91 with a #[cfg(windows)] fn as_raw_handle), and the changelog records a Windows-specific tmux -CC bug being fixed — so that path has real Windows users. A client speaks 15 CLI verbs: list, spawn, split-pane, send-text, get-text, kill-pane, proxy and more. Attach and detach are first-class (docs/config/lua/MuxDomain/attach.md). Level 2 persistence. Full VT owner. The binary is copied into the Windows zip and the Inno installer by ci/deploy.sh:115-124.

Install and drive it.

# Download WezTerm-windows-nightly.zip  -  there is no current tagged release for Windows.
wezterm-mux-server --daemonize
wezterm cli list
wezterm cli spawn -- pwsh -NoLogo
wezterm cli send-text --no-paste "claude`n"
wezterm cli get-text

What it is genuinely good at. Everything about the contract. It is the only wire format in the census explicitly designed for version-skewed client/server pairs, it was headless from the first design rather than retrofitted, get-text and send-text are exactly the two verbs a headless driver needs, and it runs on Windows, Linux and macOS from one codebase.

No tagged release since 2024-02-03 — you cannot pin a version and be on Windows at the same time. The newest tagged release is 20240203-110809-5046fc22, published 2024-02-03, two and a half years old. The rolling nightly tag that ships WezTerm-windows-nightly.zip is the only current Windows path. So the project with the most stability-conscious wire format in the census forces you onto the least stable distribution channel, and "pin the version you audited" and "run on Windows" are in direct tension. Decide which one you are willing to give up before you build on it. (One navigation note: the repository moved — wez/wezterm now 301-redirects to wezterm/wezterm.)

One unverified thing you should check first. Nobody has driven it headlessly end to end on Windows — spawn, send-text, get-text, list, with a client attaching later. Half a day settles it (Q8). If attach turns out to need the GUI, the codec is a design reference and not a product you can use. A related hazard if you run it inside another terminal: WezTerm's portable-pty loads conpty.dll by bare relative name, so the DLL search order can pick up a different terminal's bundled copy — a known cause of blank panes and broken I/O.

OpenCode's /pty API — a resumable attach cursor, on an undocumented contract and a string-typed wire

What it is. An HTTP plus WebSocket PTY API inside OpenCode (TypeScript/Bun, MIT, 192,203 stars, pushed 2026-08-02) that is documented nowhere on opencode.ai — the published server documentation has zero occurrences of /pty.

How it works. The endpoints are GET/POST /pty, GET/PUT/DELETE /pty/:id, POST /pty/:id/connect-token and GET /pty/:id/connect (WebSocket), in packages/opencode/src/server/routes/instance/httpapi/groups/pty.ts. The wire is raw UTF-8 chunks plus one control frame — a 0x00 byte followed by UTF-8 JSON — carrying the absolute output cursor after replay, so clients can resume later (packages/core/src/pty/protocol.ts). On Windows it sets useConptyDll: true (pty.node.ts); the Node path uses @lydell/node-pty, the Bun path uses bun-pty over WezTerm's portable-pty.

Persistence is level 1-plus, not level 2, and the distinction decides everything. The session survives a client disconnecting and a reconnecting client resumes from an absolute cursor rather than replaying from zero — that is a real property and the only instance of it found anywhere. But the session dies with opencode serve. It does not survive the host process, so it does not survive an ADE that launched the host.

Install and drive it.

winget install anomalyco.opencode
opencode serve --port 4096
# then, from any client:
#   POST /pty                        -> create a session
#   POST /pty/:id/connect-token      -> get a ticket
#   GET  /pty/:id/connect            -> WebSocket attach, replay, resume by cursor

What it is genuinely good at. The shape. Ticket auth, a replay cursor, REST plus WebSocket, and it ships in the Windows binary (opencode-windows-x64.zip). Because the transport is a network socket, console-independence is free — a network client needs no console at all, which is the one column where almost every multiplexer candidate is untested.

The UTF-8 text wire with a fatal decoder — it is not byte-exact. Two defects live in a 38-line file. chunks(data: string) slices at REPLAY_CHUNK = 64 * 1024 using String.slice, i.e. in UTF-16 code units, so a surrogate pair straddling a replay-chunk boundary is split into lone surrogates. And decodeInput uses new TextDecoder("utf-8", { fatal: true }) inside a bare try { … } catch { return undefined }, so invalid UTF-8 on the input path is silently dropped — the file's own comment says as much. Applied consistently, the same criterion that condemns psmux's from_utf8_lossy demotes OpenCode's wire: describing frames as "raw UTF-8 terminal chunks" does not make the payload type a byte slice, and here the payload type is a JavaScript string.

And there is no contract. Undocumented means it can change in any release, without notice, with no deprecation path — inside a project shipping at 192k-star velocity. Also unverified: nobody has confirmed the endpoints work on native Windows with the shipped binary. That is a one-hour test (Q12) — opencode serve, then POST /pty and a WebSocket connect from PowerShell. If it works you have a resumable attach surface today; if it fails, the protocol is still worth copying.

ao pty-host — detached survival, scrollback replay, resize arbitration and orphan recovery, inside a Hidden: true subcommand

What it is. A Hidden: true cobra subcommand inside Agent Orchestrator (Untrivial-ai/agent-orchestrator, Go, Apache-2.0, 8,742 stars, pushed 2026-08-02). It is a detached ConPTY host, and it is the closest thing in the census to the layer described in this guide.

How it works. conpty/spawn_windows.go spawns with CREATE_NEW_PROCESS_GROUP | DETACHED_PROCESS — the source comment says "so the host survives daemon exit," which is level-2 persistence stated as an intention rather than inferred. The wire is an 8-message binary protocol, [1-byte type][BE32 len][payload], over loopback TCP (conpty/proto.go, MsgTerminalData 0x01 through MsgKillReq 0x08); the payload is raw bytes. conpty/host.go "replays scrollback to new clients, fans output to all connected clients," and "on PTY exit it broadcasts a status update but stays alive (keep-alive, mirroring tmux behavior)." applyLargestLocked sizes the shared PTY to the largest attached client — resize arbitration that no other candidate has. And conpty/ptyregistry/registry.go persists ~/.ao/windows-pty-hosts.json with {sessionId, ptyHostPid, pipePath} so orphans are findable after metadata loss — a recovery story the other candidates simply do not have. Verbs are GetOutput(lines), Status, Kill, SendMessage. The PTY library is aymanbagabas/go-pty v0.2.3.

Install and drive it. There is no standalone pty-host to install and no install line for Agent Orchestrator was ever recorded in this research — the project ships Windows npm binary packages, so npm i -g against its published package name is the likely path, but that name was not captured and you should read it off the repository rather than off this page. The realistic route for a middle layer is not to install AO at all: it is to vendor the package (Q10), which Apache-2.0 makes legally clean. The verb surface below is what the ao CLI exposes over its loopback HTTP REST API, and it is included because it shows the shape the AO team converged on, not because it is a drive line you can paste today:

ao spawn                                  # -> POST /api/v1/sessions
ao send --session $sessionId --text 'ls'  # argument shape read off the CLI docs, not executed
ao session ls
ao session get
ao session restore
ao status --json
ao doctor --json
ao completion powershell
# Inside a spawned agent, AO_SESSION_ID / AO_PROJECT_ID let it resolve itself.

What it is genuinely good at. It is the only candidate that treats detached survival, scrollback replay, multi-client fan-out, resize arbitration and orphan recovery as one coherent design rather than four separate features. Apache-2.0 makes vendoring legally clean, which the AGPL candidates in the wider census do not.

It is not a product — Hidden: true is an internal contract, and the source says so about its security too. host_main.go carries its own caveat verbatim: "loopback bind only; any local process on this host can connect to the assigned port. A per-session random token handshake is the upgrade path." A hidden subcommand has no compatibility promise; it can be renamed or restructured in any refactor of the surrounding ADE, and the surrounding ADE offers no agent-command override (30 compiled Go packages under backend/internal/adapters/agent/), so you are not adopting the product, you are extracting a package from it. Whether that extraction is clean is untested: run go build ./backend/internal/adapters/runtime/conpty/... outside the AO tree and see what it drags in. Three hours (Q10). Clean and you have a working detached-ConPTY host to build on instead of writing one; entangled and it is a design reference.

One disambiguation, because the names collide. Untrivial-ai/agent-orchestrator — the project described here — and awslabs/cli-agent-orchestrator are different, unrelated projects.


The PTY-as-a-service lineage: terminal over HTTP, WebSocket or SSH

This is the older family: expose a PTY over a network protocol and let any client attach. The shape is right and the ancestry is instructive. Almost none of it survives contact with native Windows plus a survive-the-parent requirement.

terminado and jupyter_server_terminals — the design template. Python, BSD-3, 373 and 20 stars; terminado's last code commit was 2024-04-30 (its 2025-08-02 activity is documentation), jupyter_server_terminals moved 2026-01-14. It does run on Windows, via pywinpty. The interface is exactly the shape you would draw on a whiteboard: GET/POST /api/terminals over REST, and a WebSocket speaking ["stdout", text], ["stdin", text] and ["set_size", rows, cols]. Persistence depends on which manager you configure — NamedTermManager is level 1 (plus a cull_inactive_timeout), UniqueTermManager is level 0. Read it as a design template, not an interop standard, and read two defects with it. terminado/management.py:23-34 is a nested try/except ImportError chain, not a platform conditional: if both imports fail it binds PtyProcessUnicode = object and the failure surfaces much later as an obscure AttributeError. And pywinpty (winpty/ptyprocess.py) bridges ConPTY's non-selectable output by binding a per-PTY ephemeral 127.0.0.1 TCP listener pumped from a daemon thread — one loopback port and one polling thread per terminal, an accept() race any local process can win, output round-tripped through str, and a literal '0011Ignore' sentinel injected on empty reads. pywinpty is also Windows-only, so it is a component that costs you cross-platform uniformity, and its maintainer has stated publicly that it has >30M downloads, is "maintained by a single person," and has features "delayed… due to this lack of resources." (That statement comes from a 2025 Jupyter Discourse post whose URL was never recorded and which was not re-retrieved, so it is reported rather than verified.)

ttyd — source builds on Windows, the shipped binary does not run. C, MIT, 12,113 stars, code activity 2026-03-20. Two independent disqualifiers. Architecturally it is level 0: src/protocol.c:154,353,377-383 creates a new process per WebSocket and kills it unconditionally on close, so there is no session to survive anything. And practically, the only Windows release — ttyd.win32.exe, MinGW, 1.7.7, 2024-03-30 — fails to spawn any child on Windows 11 build 26200, which is this machine's exact build, per tsl0922/ttyd issue #1501 (whose body names "Windows 11 25H2 (build 26200)" and which closed 2026-03-19). An MSVC fix was merged 2026-03-19 and no release has been cut since; WinGet and Scoop both still pin the broken 1.7.7. If you install ttyd from a package manager today you get a binary that does not work on your OS. This is the concrete instance of trap 7 above: a Windows release artifact exists and proves nothing.

upterm — public-key authentication, and no detach. Go, Apache-2.0, 1,270 stars, pushed 2026-07-25. Everything about its Windows engineering is real: ConPTY via charmbracelet/x/conpty with Job Objects (host/internal/pty_windows.go), release artifacts for windows_{386,amd64,arm64} at v0.24.0, and a genuine nested-ConPTY lesson worth stealing — host/host_windows.go deliberately ignores os.Interrupt "to prevent upterm from dying when SSH clients send Ctrl+C to child processes via ConPTY." Its transport is SSH with public-key authentication (--authorized-keys, --github-user, --gitlab-user), which is the only real auth model anywhere in this census — every other candidate authenticates with a guessable name, a non-cryptographic token, or nothing at all.

It has no detach and no reattach, and that is the disqualifier. upterm is level 1 only: the session lives exactly as long as the upterm host process. Kill the parent and the session is gone; there is no ls verb that would list it and no path to attach a new client to an old session. It fails the single requirement that defines this whole category — surviving the process that spawned it — whatever else it is good at. (Its relay, uptermd, is also Linux-only, though the host is not.) So if you are building your own layer, upterm's auth model is the one to copy; if you are shopping for something to deploy, this is not it — and if you find yourself wanting it anyway, what you actually want is upterm's key handling bolted onto something with L2 persistence.

The rest of the lineage, and why each is off the board. gotty cannot build a PTY on Windows at all — its dependency creack/pty has start_windows.go that literally reads return nil, ErrUnsupported, and the maintained fork's release matrix ships FreeBSD, NetBSD, OpenBSD and Solaris while deliberately omitting Windows. wetty spawns every PTY, including --command, as pty.spawn('/usr/bin/env', cmd, xterm) and gates local mode on process.getuid?.() === 0, which is undefined on Windows. sshx does use ConPTY, but its author's own comment in crates/sshx/src/terminal/windows.rs reads "I can't get powershell.exe to work with ConPTY, since it returns error 8009001d," it takes no arbitrary command argument (only --shell), it has no detach, its README says "Self-hosted deployments are not supported at the moment", and every one of its releases has an empty assets array. tmate is a tmux fork and inherits tmux's POSIX dependency. code-server states plainly, in docs/install.md: "We currently do not publish Windows releases."

Two more entries belong in this family and are usually missed. Eclipse Theia (TypeScript, EPL-2.0, 21,617 stars, pushed 2026-08-01) has a Windows-capable attach-by-id implementation inside a 21k-star project: packages/terminal/src/node/shell-process.ts:90-91 is if (isWindows) { return 'cmd.exe'; }, and base-terminal-server.ts:49 is async attach(id: number) against a backend terminal registry. That is the same primitive VS Code's ptyHost provides, in a second independent implementation — so it is at minimum a peer of the prior art below. What is unvalidated is its server-mode packaging: nobody checked whether Theia's backend can be run headlessly on Windows as a standalone process, which is the only thing that would make it usable here rather than merely instructive. And ghostel (816 stars, v0.48.0 2026-07-29) is a shipped existence proof of this guide's whole architecture in a different host application — covered in its own right below.

Two entries in this family are worth reading even though you cannot run them. VS Code's ptyHost (src/vs/platform/terminal/node/ptyService.ts) is the strongest prior art in the census: PersistentTerminalProcess with detachFromProcess(id, forcePersist) at :403, a shouldPersistTerminal check at :416, an orphan-question protocol, and serializeTerminalState(ids) at :230 — and critically, replay is not a raw byte dump: it runs @xterm/headless and @xterm/addon-serialize to serialize real terminal state, and terminalRecorder.ts records resize events too, with a 10 MB MaxRecorderDataSize. Contrast Coder's agent/reconnectingpty, whose buffered backend is a 64 KiB ring dumped raw (buffered.go:52,222-223) and self-labelled "(buggy)", with its screen backend gated on runtime.GOOS == "linux". Ten megabytes of state-aware replay with resize events, versus 64 KiB of raw bytes, is the clearest illustration in this whole survey of what "replay scrollback on reattach" can mean at either end. VS Code's is level 1 across window reloads, and its open-source server packaging (openvscode-server) is Linux-only, so neither is a tool you deploy — they are the two reference points for how good or how thin a replay implementation can be.

ghostel — somebody already shipped this architecture, for a different host application

Read this one even if you never touch Emacs, because it is the closest thing in the census to an existence proof of the shape this whole guide describes. dakra/ghostel is a terminal for Emacs built on libghostty-vt with a Zig native module — 816 stars, v0.48.0 shipped 2026-07-29 — and its Windows support is not aspirational. Its own CHANGELOG.md records at [0.43.0] 2026-07-11: "Native Windows support for x86_64 and ARM64 Emacs through a ConPTY backend", and, in the same entry, "Windows releases now bundle the native module and Microsoft's redistributable ConPTY runtime… with the system ConPTY as a fallback." The backend is src/ConPtyProcess.zig, and every release ships ghostel-conpty-*.dll plus ghostel-openconsole-*.exe — which is bundling done exactly the way item 3 of the build spec says to do it, by a project that had to make it work.

Architecturally it is your shape, with the names changed: the native module owns ConPTY and the host application is a swappable frontend. Emacs is the disposable client; the module is the durable connection-owning layer. That is the same split as Evennia's Portal/Server and the same split you are trying to insert under ORCA — and unlike almost everything else in this guide, somebody has already built it, versioned it, and shipped Windows binaries of it. It is not a middle layer you can spawn from an ADE, so it is not a candidate; it is the proof that the load-bearing part is buildable on native Windows, plus a working reference for the ConPTY-plus-bundled-runtime pattern. It is also the subject of Q21 — whether that ConPTY backend means libghostty-vt works on Windows or means ghostel routed around it, which is one hour of reading src/ConPtyProcess.zig and decides whether you have three VT-parser options on Windows or two.

Interface shapes: the eight ways a middle layer can be driven

Once you have decided that something must own the PTY, the next decision is how anything talks to it. Every shape below answers the same three questions differently: what does a caller need in order to speak it (a shell? a generated client? a socket and a framing library?), who owns the lifetime of the connection (the caller, the callee, or neither), and what happens to the terminal bytes on the way through (passed verbatim, re-encoded as text, or never produced at all). Get those three right for your caller — which, for a headless-first middle layer, is a script, a hook or an agent, not a human — and the shape picks itself.

They are not mutually exclusive. The best-shaped candidates offer two or three, and the strongest single design decision in the whole census (WezTerm's versioned codec) is about tolerating change in one of them.

The eight below are seven distinct machine interfaces plus SSH, which is a transport rather than a framing but has real candidates behind it and so is treated as a shape of its own.

The install-and-drive lines in this section carry the same caveat as those in the candidate sections above: they are assembled from each project's own docs and were not executed on a Windows machine in any pass of this research. Treat them as the shape of the invocation, not as a transcript.

CLI verbs

What it is. The tool ships an executable with subcommands, and you drive it by spawning processes: psmux ls, wezterm cli list, rmux new -d -s probe.

What it buys. Trivially scriptable from anything, with no client library. The same verb string survives being called by a shell, a hook, an agent or a human, and it composes with --jsonPane's runpane emits --json on every verb and ships a machine-readable contracts/runpane/contract.json plus a first-party Python client, documented in docs/RUNPANE_CLI_CONTRACT.md, and Agent Orchestrator's ao status --json / ao doctor --json do the same over a loopback HTTP API with ao completion powershell for the shell. This is the only shape where a script, a hook and a human all speak one language, which is the property that matters when the human leaves the loop: nothing has to be re-learned. tmux, psmux and rmux sit in that pole by construction (rmux ships 90+ tmux-compatible verbs); Zellij deliberately does not, its README committing to "must not sacrifice simplicity for power" with an on-screen hint bar as the primary interface and CLI verbs secondary.

What it costs. Process-spawn latency per call, no push and no streaming, and state you must re-fetch rather than be told about.

Who offers it. tmux, psmux, rmux (90+ verbs), wezterm cli (15 verbs), ao (a thin client over HTTP), Pane's runpane, qscreen's qscn, herdr, Zellij. All of them run on Windows.

# Drive the verb surface — one detached session, then prove it is listed
psmux new-session -d -s probe ; psmux ls
rmux  new -d -s probe         ; rmux ls
wezterm-mux-server --daemonize ; wezterm cli list
zellij list-sessions          # NOTE: no verified detached-start invocation exists for Zellij -
                              # see the Zellij entry. `zellij --session X options --help` only
                              # prints help; the interactive path is `zellij attach --create X`.

What it implies for an ADE-agnostic layer. Verbs are the lowest-friction interface an ADE-independent layer can expose, and they are also the shape whose headless behaviour is least established — see the no-console column below, which is the one thing that would disqualify all of them at once.

tmux control mode (-C / -CC)

What it is. A line-based machine protocol, documented in tmux.1 under CONTROL MODE: you send tmux commands on stdin, and it replies with %begin / %end / %output / %window-add / %layout-change lines on stdout. The outer program owns rendering; tmux owns sessions.

What it buys. A real, decade-hardened contract with at least five implementers, not one — iTerm2, WezTerm (verified in source at wezterm-escape-parser/src/tmux_cc/tmux.pest, mux/src/tmux.rs and tmux_pty.rs:90-91 with a #[cfg(windows)] fn as_raw_handle, and a changelog entry recording a Windows-specific tmux -CC bug being fixed), psmux (src/main.rs:4194, src/control.rs at 554 lines, docs/control-mode.md at 432 lines, three PowerShell tests), Tomiyou/ivyterm and paulrobello/par-term. Critically, %output carries raw pane bytes, so the "parse once, let the last hop render" property is available: your ADE's own xterm.js can be the only VT parser in the chain.

What it costs, and this is the characteristic failure. A line-based text protocol is fragile to interleaving, and ConPTY corrupts it when nested: it silently consumes the DCS escape sequences the protocol is detected with, and interleaves its own cursor-positioning sequences into the output. The verbatim source quote, its SSH antecedent, Microsoft's independent corroboration in microsoft/terminal#19621, and the disclosure that generalising the hazard beyond SSH is inference rather than the source's claim are all under Nesting. The remedy the source gives is SSH-specific and worth quoting because it names the shape of the cure: "the SSH client must disable PTY allocation so that stdin/stdout are raw pipes: ssh -T user@host tmux -CC."

Two further costs are documented rather than inferred. docs/control-mode.md:391 states "ConPTY may normalize line endings and process certain cursor movement sequences internally. %output data may look slightly different from what a Unix tmux session would produce" — a straight Windows/Linux behavioural divergence in the interface you would be standardising on. And psmux's src/server/mod.rs:1225 runs String::from_utf8_lossy(&bytes) on each ring drain, so a multi-byte character split across two drains becomes U+FFFD permanently — the same string-typed-wire failure class described under Nesting.

The order to test in. Before asking whether corruption happens, ask whether the cure is reachable: ORCA spawns through node-pty, which is always a ConPTY, and the documented -CC remedy is "give the child raw pipes." If agentCmdOverrides cannot produce a raw-pipe child, the corruption question is moot and this interface is unavailable in this topology regardless. That is two hours of reading ORCA's spawn path from src/shared/tui-agent-launch-command.ts:30 through to the node-pty call (Q1a), and it can make the afternoon-long corruption experiment (Q1) unnecessary.

# A genuine raw-pipe parent — the topology -CC actually wants.
$psi = [Diagnostics.ProcessStartInfo]::new('psmux', '-CC new-session -s probe')
$psi.RedirectStandardInput  = $true
$psi.RedirectStandardOutput = $true
$psi.UseShellExecute        = $false
$p = [Diagnostics.Process]::Start($psi)
$p.StandardInput.WriteLine('list-panes')   # commands in

# A single ReadLine() returns ONE line and cannot show you a %begin/%end block.
# Drain with a deadline instead, and keep the transcript to diff later.
$deadline = (Get-Date).AddSeconds(5)
$lines = while ((Get-Date) -lt $deadline -and -not $p.StandardOutput.EndOfStream) {
    $p.StandardOutput.ReadLine()
}
$lines | Tee-Object -FilePath "$env:TEMP\cc-rawpipe.txt"
# You should see a %begin line, one or more %output lines, and a matching %end.
# Now run the SAME command as an agentCmdOverrides target inside the ADE, capture to a
# second file, and diff them — the difference is the corruption Q1 is asking about.

Binary wire protocols

What it is. A framed binary channel with an explicit message catalogue, spoken over a pipe or a socket, with no text layer to corrupt.

What it buys. Exact byte fidelity, low overhead, and — in the one case that was designed for it — tolerance of version skew between client and server. WezTerm's codec is the standout: codec/src/lib.rs encodes "length, ident and serial number… using a variable length integer encoding… client and server can more gracefully manage unknown enum variants." That is precisely the ADE-churn insulation this whole project exists to buy, written down and shipped by somebody else.

What it costs. You write and maintain the client. There is no ecosystem, no generated bindings, no curl equivalent, and nobody else's tooling speaks it.

Who offers it. The WezTerm mux codec (MIT, inside a 28.1k-star project, Windows-capable, L2). ao pty-host's conpty/proto.go[1-byte type][BE32 len][payload], eight messages from MsgTerminalData 0x01 to MsgKillReq 0x08, Apache-2.0, which makes vendoring legally clean. qscreen's ScreenFrame / ScreenRun, structured runs with FRAME_FLAG_*, FrameColor and FrameMouseMode — the same "parse once, render at the last hop" idea as tmux control mode, arrived at independently. And asciinema's ALiS: four negotiated WebSocket sub-protocols (v1.alis binary with the 5-byte magic ALiS\x01 = [0x41,0x4C,0x69,0x53,0x01] plus LEB128 framing, v2.asciicast, v3.asciicast, raw), where the relay "maintains comprehensive state for each active stream by running the whole stream through asciinema's own virtual terminal emulator" — late-joiner screen sync for free from an existing open-source relay. The asciinema CLI is out on Windows (v3.2.1 release assets are darwin and linux only, even in the from-scratch Rust rewrite); the ecosystem is not. How much code a native-Windows ALiS producer would take is genuinely unknown — no estimate here survives scrutiny, so do not plan against one.

A twenty-year-old warning that lands directly on an Electron ADE. The BBS-era DOOR32.SYS convention handed a child process an already-open Winsock socket handle number rather than emulating a serial device — the "pass a handle, don't emulate" precedent. ENiGMA½ issue #175closed since 2018-12-09, so this is history rather than an open defect — records what happened when a Node host tried it: "socket descriptors cannot be shared" in Node.js, and the project's own documented workaround is bivrost!, a Rust sidecar between the Node host and the handle. ORCA is Electron and Node. The BBS world hit this exact wall eight years ago and solved it with the Rust-daemon shape that psmux, oly and qscreen already occupy.

HTTP and WebSocket

What it is. A local HTTP server for control plus a WebSocket (or SSE) for the byte stream.

What it buys. Every language has a client. Auth, TLS and proxying are solved problems with existing answers. Push comes free with WS or SSE, and OpenAPI gives you a generated client — opencode serve publishes OpenAPI 3.1. The console-independence is inherent: a network client needs no console at all, which is the one column where the PTY-owning CLI candidates are all untested.

What it costs. Port management, bind address and authentication become your problem, and a stray 0.0.0.0 is a real leak. Loopback binding is not an authentication boundary either — ao pty-host carries its own caveat in host_main.go: "loopback bind only; any local process on this host can connect to the assigned port. A per-session random token handshake is the upgrade path."

Who offers it. OpenCode's /pty is the best-shaped of them — GET/POST /pty, GET|PUT|DELETE /pty/:id, POST /pty/:id/connect-token, GET /pty/:id/connect (WebSocket), with ticket auth and a replay cursor, shipped in the Windows binary. Also oly (named-pipe IPC plus HTTP/WS plus push notifications); kandev, whose ws://localhost:38429/ws endpoint is documented in its own docs/public/websocket-api.md; Jupyter's terminado (GET/POST /api/terminals plus a WS carrying ["stdout",text] / ["stdin",text] / ["set_size",r,c]); Agent Orchestrator's /api/v1/*; and the x3270 family's -httpd REST control plane, worth a glance because it is a headless scriptable emulator with a three-transport control plane that already works on Windows. The evidence for that last one is a set of counts from a GitHub code search over the x3270 source tree — 179 hits for httpd, 69 for scriptport, and zero for CreateNamedPipe — so on Windows its control plane is loopback TCP plus HTTP REST. Those are snapshot counts from one search with no query string recorded; the zero is the load-bearing part and it is the one you can re-run in a minute.

The characteristic failure of this shape is the undocumented contract. OpenCode's /pty API appears nowhere on opencode.ai — the published server doc has zero /pty occurrences — so the whole surface can change without notice, in a project shipping at 192k-star velocity. And its wire is UTF-8 text, not raw bytes, with the surrogate-splitting and silent-drop consequences set out in the OpenCode entry above: describing frames as "raw UTF-8 terminal chunks" does not make the payload type a byte slice.

A second characteristic failure: a rich HTTP catalogue that does not include the verb you need. Warp's crates/local_control — in warpdotdev/warp, the AGPL-3.0 client, branch master — is the closest open-source analogue to tmux control mode found anywhere: a versioned action catalog (crates/local_control/src/catalog.rs:4, PROTOCOL_VERSION: u32 = 1) over authenticated loopback HTTP at /v1/control, with 84 entries carrying status: Implemented covering pane.list/split/resize/focus/navigate/close, session.list/activate, input.insert/replace and more. Reading the whole catalogue confirms the negative: no output-read, no send-keys-to-PTY, no attach. It is a window-management control plane, not a PTY-attach protocol. Its auth also leans on a kernel-reported peer-UID check, which is POSIX-only. (Note which Warp this is: the client is open source and this crate was read at source. Warp's commercial product and its Oz cloud API are closed and nothing about those was verified anywhere in this guide.)

# Control plane: ordinary REST, so PowerShell alone is enough.
opencode serve --port 4096                       # then, from another shell:
$pty   = Invoke-RestMethod -Method Post -Uri 'http://127.0.0.1:4096/pty'
$id    = $pty.id                                 # field name unverified - inspect $pty
$ticket = Invoke-RestMethod -Method Post -Uri "http://127.0.0.1:4096/pty/$id/connect-token"
$ticket

The attach is the part that actually tests anything, and PowerShell has no built-in WebSocket client, so it needs System.Net.WebSockets.ClientWebSocket directly. This is the step that exercises the replay cursor — the property the whole entry is about — and it is what Q12 means by "a WS connect from PowerShell":

Add-Type -AssemblyName System.Net.WebSockets.Client -ErrorAction SilentlyContinue
$ws  = [System.Net.WebSockets.ClientWebSocket]::new()
$uri = [Uri]"ws://127.0.0.1:4096/pty/$id/connect?token=$($ticket.token)"   # param name unverified
$ws.ConnectAsync($uri, [Threading.CancellationToken]::None).Wait()
"state: $($ws.State)"                            # you should see: Open

$buf = [ArraySegment[byte]]::new([byte[]]::new(64KB))
$r   = $ws.ReceiveAsync($buf, [Threading.CancellationToken]::None).Result
"first frame: $($r.Count) bytes, type $($r.MessageType)"
# The control frame is a 0x00 byte followed by UTF-8 JSON carrying the absolute output
# cursor after replay. If $buf.Array[0] is 0, you are looking at it - decode the rest as
# JSON and that number is what a later client resumes from.
$ws.Dispose()

Both blocks are written against an undocumented API and neither has been run. The endpoint paths are verified in source; the request and response bodies, the field names and the token parameter are not documented anywhere and are guesses from the route handlers. Read packages/opencode/src/server/routes/instance/httpapi/groups/pty.ts for the current schema before trusting either block — and if you would rather not fight PowerShell's WebSocket API, this step is three lines in Node or Python and there is no reason to be precious about it.

MCP

What it is. The agent-facing shape: the middle layer registers as an MCP server and the coding agent calls its tools directly.

What it buys. The agent can drive the middle layer itself, with no glue code, and the tools are discoverable. quil exposes 18 MCP tools — the richest agent-facing surface in the census.

What it costs, and it is a lifetime problem, not a protocol one. A stdio MCP server cannot own persistent sessions, because the client spawns it — the sessions die with the client that was supposed to be swappable. Only a server the middle layer runs independently works. kandev is the one candidate that names this distinction in its own docs (docs/public/automation-and-mcp.md), offering four MCP modes including an External MCP mode explicitly "for a client outside a task." dagger/container-use is the counterexample that proves the rule: it registers as container-use stdio, a stdio MCP server, which cannot own persistent sessions at all.

Who offers it. quil (18 tools), kandev (External MCP), the paseo daemon, ripple, and vibe-kanban (dead — sunset 2026-04-24).

What it implies for an ADE-agnostic layer. MCP is a good additional surface and a bad only surface. The test to apply to any MCP-shaped candidate is one question: does the client spawn this server, or does it connect to one that was already running? If the former, the persistence you are shopping for is not there.

JSON-RPC over stdio or a pipe

What it is. Line-framed JSON-RPC between a host and a spawned process. ACP (Agent Client Protocol) is the ecosystem's convergence point; Codex's app-server is OpenAI's variant, self-described as "JSON-RPC lite" with Thread / Turn / Item primitives.

What it buys. The agent-CLI ecosystem is converging here — ACP has 3,840 stars and implementations across Zed, JetBrains, Copilot CLI, Gemini CLI, Claude Code, Neovim and Toad — and the spec is transport-agnostic by its own text.

What it costs. Subprocess-bound lifetime, unless you add a network transport. Codex has one: --listen ws://IP:PORT is a standing listener the client does not own, and its app-server-transport/src/transport/mod.rs:27-32 registers remote_control, stdio, unix_socket and websocket unconditionally. But app-server/README.md labels websocket "experimental / unsupported. Do not rely on it for production workloads," and codex-rs/app-server-daemon/README.md states "The current daemon implementation is Unix-only… does not yet support Windows lifecycle management." The transport is there on Windows; the daemon lifecycle is not. Release rust-v0.147.0-alpha.4 does ship codex-app-server-x86_64-pc-windows-msvc.exe, and the server ships /readyz, /healthz, backpressure returning -32001, and generate-ts / generate-json-schema for a version-pinned schema.

What it implies for an ADE-agnostic layer. ACP's own draft transports chapter states "The protocol is transport-agnostic… can be implemented over any communication channel that supports bidirectional message exchange," and names Streamable HTTP as transport 2, formally sanctioning custom transports. So ACP over a Windows named pipe is spec-conformant. Whether it builds and passes on Windows is a different question — see the negative-space section below.

SSH

What it is. The session is reached by SSH, and the middle layer is the SSH server.

What it buys. The only interface in the census with a real authentication model rather than a local-trust assumption. upterm's host takes --authorized-keys, --github-user, --gitlab-user — public-key auth, verified in its flag set, and nothing else in the census matches it. The client side is universal and already installed on Windows 11.

What it costs. Everything else, in the two candidates that offer it. upterm is L1 only: the session lives exactly as long as the upterm host process, with no detach and no reattach, which is the single requirement that defines a middle layer — so it fails on the one axis that matters most, whatever else it does well. Its ConPTY engineering is genuinely good and its nested-ConPTY lesson is worth stealing whatever you pick; both are in the upterm entry above. Its relay, uptermd, is Linux-only. tmate is a tmux fork and inherits tmux's POSIX dependency outright.

And the in-box option does not fill the gap. Windows OpenSSH Server has no tmux equivalent — that is stated by a Microsoft member (SteveL-MSFT) in Win32-OpenSSH#2291, still open: "On Linux, you can use something like tmux to keep a long lived session, but the equivalent doesn't exist on Windows." The mechanism usually given for why — that sshd puts children in a Job Object and disconnect tears it down, so there is no SIGHUP to trap — rests on one maintainer sentence with zero code-search corroboration: searches for CreateJobObject and KILL_ON_JOB_CLOSE in openssh-portable return no hits. The absence of a tmux equivalent is established; the Job-Object explanation for it is not, and should not be repeated as though it were.

# No angle brackets: `<` is a reserved redirection operator in PowerShell.
$githubUser = 'your-github-username'
upterm host --github-user $githubUser -- pwsh    # public-key auth; session dies with this process

The no-PTY NDJSON channel

What it is. Not a way to talk to a PTY owner — a way to have no PTY at all. claude -p --input-format stream-json --output-format stream-json --verbose is a full-duplex NDJSON channel over ordinary pipes, with --include-hook-events, --forward-subagent-text (carrying parent_tool_use_id), --include-partial-messages and --replay-user-messages as companions.

What it buys. Console-independence by construction — ordinary pipes, no console needed anywhere, which makes it the only option in this document that passes the headless test without anyone having to run an experiment. Three channels stack on top of it, all set out with their sources in When you need a session daemon above: Claude Code hooks as the write path, five types rather than the three that get repeated in blog posts, where PreToolUse returns a permissionDecision plus an updatedInput that replaces the tool's arguments, and where the http type means your middle layer can BE the endpoint — a long-lived process you own, which sidesteps the stdio-MCP lifetime problem entirely; OpenTelemetry as the read path, including claude_code.tool.blocked_on_user, the "is the agent stuck waiting on a human" signal, with no PTY anywhere; and the JSONL transcript tree, from which subagent fan-out topology is readable on the filesystem alone.

What it costs. It buys everything except the two things PTY ownership uniquely provides: the byte stream of a program that insists on a terminal, and a human typing into a live session. Five narrower limits are load-bearing, and all five are quoted from the documentation in the opening section: http hooks are unavailable on SessionStart and Setup, the two bootstrap events a middle layer most wants to own; mcp_tool hooks on those same two events "should expect the 'not connected' error on first run", which leaves command as the only dependable bootstrap hook type; as of v2.1.199 a tool marked _meta["anthropic/requiresUserInteraction"] can refuse to have its approval prompt skipped by a hook; Claude Code strips OTEL_* exporter variables from every subprocess it spawns, so telemetry config does not reach grandchildren; and the JSONL tree is undocumented, version-fragile internals that cleanupPeriodDays garbage-collects.

And one cost that is architectural rather than a limit in the documentation: this route is per-vendor plumbing. Every channel named above — claude -p stream-json, hooks, the claude_code.* telemetry, the JSONL tree — is Claude Code's. Codex's equivalents (exec --json, rollout JSONL, resume --last) are a second implementation, not the same one. You do not install a no-PTY middle layer; you implement one per agent vendor, and each vendor's config surface changes under you on its own schedule. That is the direct trade against the PTY-owning route, which is agent-agnostic by construction because it does not care what runs inside it. If "any ADE, any agent CLI" is a requirement rather than a preference, this is where the no-PTY route charges you for its console-independence.

And the cross-platform claim is inference, not measurement. The flags are documented without platform qualification and the transport is ordinary pipes, but nobody has run this on both Windows and Linux and compared output. That is the load-bearing cross-platform claim of the whole no-PTY architecture and it rests on an inference.

One more channel, narrow but real. terminalSequence (v2.1.141+) lets a hook push escape sequences out through Claude Code's own terminal write path: "Hooks run without a controlling terminal, so writing escape sequences directly to /dev/tty fails… This is race-free, works inside tmux and GNU screen, and works on Windows where there is no /dev/tty." The allowlist is OSC 0/1/2 (titles), OSC 9 (including 9;4 taskbar progress), OSC 99, OSC 777 and a bare BEL, restricted "to sequences that can't move the cursor or alter colors" — so OSC 133 is not available — but it is enough to push out-of-band structured signals into the PTY byte stream that a nested middle layer parses natively.

Cross-vendor convergence is itself evidence. Codex ships the same shape independently: exec --json plus rollout JSONL at ~/.codex/sessions/YYYY/MM/DD/rollout-*.jsonl with resume --last. Two vendors landing on the identical design says this is a real architectural pattern, not one company's idiosyncrasy.

# The channel itself: NDJSON in, NDJSON out, ordinary pipes, no console anywhere.
#
# Set the outbound encoding FIRST. Piping a string to a native executable goes through
# [Console]::OutputEncoding, which is not UTF-8 by default in Windows PowerShell 5.1 - so
# the one document whose central warning is "string-typed wires destroy Unicode" can lose
# bytes in its own example. Use pwsh, and set it anyway:
$OutputEncoding = [Text.UTF8Encoding]::new($false)
[Console]::OutputEncoding = [Text.UTF8Encoding]::new($false)

'{"type":"user","message":{"role":"user","content":"list the files"}}' |
  claude -p --input-format stream-json --output-format stream-json --verbose --include-hook-events

# The read-only observation channel — note the master switch is not an OTEL_* var:
$env:CLAUDE_CODE_ENABLE_TELEMETRY = '1'
# traces additionally need CLAUDE_CODE_ENHANCED_TELEMETRY_BETA=1;
# the claude_code.hook span needs ENABLE_BETA_TRACING_DETAILED=1 + BETA_TRACING_ENDPOINT + org allowlisting.
# Content is redacted by default: opt in per-field via OTEL_LOG_USER_PROMPTS, OTEL_LOG_TOOL_DETAILS,
# OTEL_LOG_TOOL_CONTENT, OTEL_LOG_ASSISTANT_RESPONSES, OTEL_LOG_RAW_API_BODIES (=file:<dir> writes
# untruncated bodies to disk with a body_ref path).

Honest scoping. This is print/headless mode. It does not attach to an existing interactive TUI. It is a way to build a middle layer that spawns the agent with no PTY at all — a different architecture, not a different transport.


The verb set an ADE-agnostic middle layer has to expose

Whatever shape you pick, the same primitives keep showing up. The list below is derived from the union of every candidate's verb setruntimeselect.Runtime in Agent Orchestrator, wezterm cli, the tmux/psmux/rmux verbs, OpenCode's /pty, and quil's MCP tools. It is a synthesis and a judgement, not any one project's API.

Verb Why it is non-negotiable
spawn(cmd, cwd, env, cols, rows) → session_id The ADE hook point. ORCA's agentCmdOverrides is exactly this
list() → [{id, alive, pid, exit_code, cwd, title}] Discovery without state in the ADE
attach(id, {cursor}) → {replay, cursor, stream} The one primitive nothing else substitutes for. Must carry a resumable cursor. OpenCode's protocol.ts is the model for the cursor semantics only — copy its absolute-output-cursor control frame, not its wire type, because its payloads are JS strings that split surrogate pairs at 64 KiB and drop invalid UTF-8 input. Carry bytes, not string
detach(id) Distinct from kill; the thing dtach and abduco exist for
send(id, bytes) and send_keys(id, keys) Raw bytes for fidelity, named keys for scripts
get_output(id, lines) / snapshot(id) → {text, cursor_x, cursor_y} Headless read without attaching. quil's MsgScreenshotPaneResp is the shape
resize(id, cols, rows) Must be explicit on Windows — there is no SIGWINCH. Multi-client needs an arbitration rule, and Agent Orchestrator's applyLargestLocked is the only one found
status(id) → {alive, pid, exit_code, blocked_on_user?} Headless liveness. The blocked_on_user bit is what OTel gives you free
kill(id, signal) With a process-tree story: Job Objects on Windows (upterm, rmux, kandev's cmd/winjob/), not a bare TerminateProcess
wait(id, {idle|text|exit}, timeout) The primitive that makes it scriptable. boo, rmux and andyk/ht all shipped this independently

Two properties beyond the verb set, both learned the hard way, and both stated here as build requirements rather than as findings — the evidence behind them is in Unauthenticated local IPC below.

Namespacing must be identity-scoped, not merely selectable. rmux's \\.\pipe\{prefix}-{sid}-il-{integrity}-{label} is the only correct implementation found, because the name is computed from the token rather than picked from a string, so it cannot be entered by a process that merely guesses. A configurable name is not access control: oly has the knob (OLY_SOCKET_NAME, src/config.rs:123-126) and still has no ACL on the pipe. Namespacing prevents accidents; ACLs prevent attacks. You need both, and only rmux has both.

A local transport is not an auth boundary. Rank the transports before you pick one: a properly ACL'd named pipe with FILE_FLAG_FIRST_PIPE_INSTANCE is strictly stronger than an AF_UNIX socket on Windows — where chmod is a no-op, SO_PEERCRED does not exist, and the only protection is the ACL on the parent directory — which is in turn stronger than a loopback TCP port, reachable by any local process including other users and low-integrity ones. And note the ceiling on all of them: authentication in this category is per-user, not per-process, so ORCA, every agent CLI, every MCP server and every npm postinstall script run as the same Windows user and can all reach the same endpoint. That is the same trust model as tmux's 0700 socket — a property of the category, not a scandal about any one project.

The convergence: three ecosystems, one shape

Three unrelated projects, solving unrelated problems, independently arrived at the same architecture.

  • OpenCode, in its own server documentation: "When you run opencode it starts a TUI and a server. Where the TUI is the client that talks to the server… This architecture lets opencode support multiple clients and allows you to interact with opencode programmatically."
  • Evennia, a MUD engine, splits Portal from Server over a Twisted AMP socket: "you can fully reload the Server and have players still connected to the game. One [sic — upstream typo for 'Once'] the server comes back up, it will re-connect to the Portal and re-sync all players as if nothing happened." Portal owns every protocol and every persistent connection; Server owns all logic and can crash or reload freely. That passage is on Evennia's Portal-And-Server documentation page, not on the docs landing page — search the docs for "Portal And Server"; the exact page URL was not recorded here and the landing page does not contain the quote.
  • nREPL, from the Clojure world, "solves the 'narrow waist' problem… shares a lot with the Language Server Protocol, though it predates LSP by several years… Writing an nREPL server… can be done in a couple hundred lines." Self-describing, session-oriented, no PTY. That passage is in the nREPL specification rather than on the site root, which redirects.

A coding agent, a text-game engine and a Lisp REPL. Read this convergence as a stronger signal than any individual tool finding in this guide — three unrelated ecosystems, none of which had heard of your problem, all splitting the disposable frontend from the durable connection-owning process. Evennia already has the name for the thing you are building: Portal. SLIME/swank states the same idea in one sentence: the frontend is disposable, the real long-running state lives behind a socket. That is the ADE-swap requirement, named and shipped, three times over.


What does not exist

This is the part that saves you a week. Each item below was searched for and not found, and the reason each absence matters is stated with it.

No terminal-control standard has emerged. MCP has zero terminal or PTY SEPs among the 41 Final ones at modelcontextprotocol.io/seps — that count is verified. Alongside it sits an unquantified observation from GitHub search rather than a measured figure: there are many independently-named mcp-terminal-server repositories with no shared schema between them, and no count was recorded, so re-run the search rather than quoting a number. One correction is worth carrying because the opposite is widely believed: OpenAI's Codex-harness post is not evidence that MCP was tried for terminals and abandoned — it never mentions terminals or PTYs. What it says is that "maintaining MCP semantics in a way that made sense for VS Code proved difficult," i.e. MCP was abandoned as a VS Code integration protocol. That is weaker evidence, and only suggestive of the terminal case.

tmux control mode is a contract, not a specification. It standardises the framing — commands in, %begin / %end / %output / %window-add / %layout-change out — and it has at least five implementers, so it is a genuinely reusable contract rather than one project's internal API. An earlier belief that no second production consumer existed was wrong: WezTerm, ivyterm and par-term all implement it, and WezTerm exercises it on Windows. Where it stops: there is still no specification independent of tmux's implementation. The definition is whatever tmux.1 and the tmux source do, which means version skew is a diff against a moving implementation rather than against a document — the exact problem WezTerm's varint codec was built to solve for its own protocol.

ACP standardises the conversation, not the process. Its session verbs are session/load (which replays history), session/resume, session/list, session/fork and session/close. Read those carefully: that is conversation-state continuity, not live-process attach. There is no primitive in ACP today that attaches to a running PTY. Two further limits: every job in .github/workflows/ci.yml is ubuntu-latest, so there is no Windows CI at all, and its terminal work is in flight rather than shipped (schema/src/v2/terminal.rs, docs/rfds/v2/terminal-output.mdx). The spec does formally sanction custom transports, so ACP over a Windows named pipe is conformant — but conformant and tested are different words, and a two-day prototype against the reference implementation is what would settle whether it builds and passes on Windows.

No Windows analogue of reptyr, after 10+ years. reptyr is the POSIX solution to the structurally identical problem — grab the I/O of a process you did not spawn — via ptrace. No attempted port was located, which is an absence-of-evidence finding from repository and issue searches rather than an exhaustive proof. Read it as evidence that the platform makes this hard, not that the niche is unfilled: the mature Windows implementations all pick one half of late-attach, NVDA reading without writing and winpty/wexpect reading and writing but owning the console from birth, and the halves never combine.

No native network-attach for Claude Code, and it has been declined twice — the two not_planned closures are in the opening section. Betting on native network-attachable Claude Code sessions is a weak hypothesis, which is itself an argument for an external layer.

No compatibility matrix — from Microsoft or anyone — for running a newer bundled conpty.dll / OpenConsole.exe against an older or locked-down Windows Server Core or LTSC host. This matters because bundling the ConPTY NuGet package is the recommended path, and the recommendation is a refusal to backport rather than an endorsement; the quote is under The four things a middle layer must get right on Windows.

No primary Microsoft source pins the build thresholds for WIN32_INPUT_MODE (its spec was authored 2020-05-07) or for VT passthrough — build >= 22621 is corroborated only by two independent Rust implementations, psmux and rmux, converging on the same magic number.

The xterm.js attach-addon convention went unexamined, and it is the single largest known gap in this guide's protocol coverage. It is easy to describe it as the most widely deployed de-facto terminal wire convention in existence — ttyd, code-server and VS Code all use it — but that superlative is unsourced and untested here, and it survives only as a lead.

No open-source SCADA or industrial session manager with a persistent-session-plus-attach design was located (Q20). That area is genuinely under-researched, not confirmed absent — recorded so the gap is not mistaken for a finding.


The column that can disqualify every PTY candidate at once

Before the tables, read this, because one column in them is different from all the others.

"Verbs with no console attached" is the headless-first requirement stated as a testable property, and the honest answer is untested for every single PTY-owning candidate. zellij action, tmux send-keys, wezterm cli and the rest are normally invoked from inside a terminal. Whether they still function when a Windows Service, a Scheduled Task or a Claude Code hook invokes them with no console attached and no TTY on stdin or stdout is a completely different question, and nobody has run it.

It matters more than its size suggests, for two reasons. It is disqualifying if it fails — a middle layer you can only drive from inside a terminal is not headless-first, it is just a multiplexer. And it is cheap: roughly two hours (Q23) to settle for the entire field at once. Those two facts together are why it is the first thing to run, ahead of the week-long Q9 that has more leverage — see The running order, with wall-clock. If those verbs work, the whole "headless verbs" column becomes trustworthy. If they fail, those candidates are terminal tools rather than middle layers, and the no-PTY architecture wins by default — because it is the only row that passes this column by construction, over ordinary pipes, with no console needed anywhere.

The test is step 3 of The verification ritual, and two things about reading its result are worth stating here. A task result of 0x0 proves nothing — the task can report success while the verb wrote an error to a stream nobody captured, or exited early because it could not open a console. What proves it is the output file: it must exist, and it must contain the session list you created from an interactive shell beforehand. And same-principal matters: run the probe as the same user that owns the daemon, or you are testing namespacing rather than console-independence.


Comparison table: candidates that own a PTY

How to read this. Star counts and last-activity dates churn weekly; every number is a snapshot taken 2026-08-02. "Last activity" is the repository's pushed_at unless the cell says otherwise. "Survives client death" uses the persistence levels from Persistence scope: L0 dies with the connection, L1 survives client disconnect but dies with the host process, L1+ is L1 plus cursor-resumable replay, L2 survives the ADE that spawned it, L3 survives a reboot. Only one L3 claim exists anywhere in the census, and it is macOS-only. The table scrolls horizontally; the two columns that decide most cases are Survives client death and Verbs, no console.

Candidate Language Licence Stars Last activity Transport Native Windows Survives client death Interface shapes offered Nesting behaviour Headless verbs Verbs, no console Installable standalone
psmux Rust (a PowerShell installer/test suite inflates the language stats; the product is the Rust binary) MIT 3,140 2026-08-02 Loopback TCP, app-layer AUTH <key> handshake Yes L2 CLI verbs + tmux -C/-CC control mode Own guard plus PSMUX_ALLOW_NESTING=1but the error text names PSMUX_SESSION, the wrong variable, so a scripted wrapper that follows the message will not clear the guard. Pipe mode negotiates size in band via CSI 18 t Full tmux verb set + -CC Untested Yes — Windows only (CI builds only *-pc-windows-msvc; the ubuntu/macos job runs two shell scripts, not a build)
Zellij Rust MIT 34,647 2026-08-01 Named pipes (interprocess) Yes (CreatePseudoConsole/ResizePseudoConsole/HPCON in zellij-server/src/os_input_output_windows.rs) L2 — spawns with CREATE_NO_WINDOW | CREATE_NEW_PROCESS_GROUP, but note neither flag implements parent-death survival; on Windows a child does not die with its parent by default CLI verbs + protobuf-schema'd WASM plugins. No verified detached-start invocationzellij --session X options --help only prints help The only candidate with a policy knob: nested_session_handling = ask/fullscreen/descend/never CLI + plugins Untested Yes — msvc zip + MSI, plus Linux and macOS
rmux Rust, 12 crates, 21 MB Dual-licensed; the pair is not stated in repo metadata (GitHub reports NOASSERTION) — read LICENSE-* at the tag you would vendor 2,533 2026-07-26 Named pipes, namespaced by SID + integrity level, FILE_FLAG_FIRST_PIPE_INSTANCE, ImpersonateNamedPipeClient Yes L2 90+ tmux-compatible verbs + typed Rust/Python/TS SDKs + Ratatui widget + E2E-encrypted web share Untested 90+ verbs + typed SDKs Untested Yes — winget, scoop, choco
herdr Rust Apache-2.0 23,466 2026-08-01 Named pipes (interprocess) Beta only — preview channel L2 (beta) — "Local persistent sessions | beta" is listed under Supported CLI + HTTP API (src/api/server.rs) + third-party TS/Python SDKs "Nested launch override | beta", listed Supported. Separately, "Prefix input-source switching | unsupported" is the only primary-source statement about prefixes on Windows found anywhere CLI + HTTP Untested Preview channel only on Windows — the tag preview-2026-07-29-44b3adb12552 (dated preview plus commit suffix; the bare preview-2026-07-29 does not resolve) ships herdr-windows-x86_64.zip; stable v0.7.5 has no Windows asset
oly (slaveOftime/open-relay) Rust MIT 89 main HEAD 2026-07-01; pushed_at 2026-07-30 (four side branches) Named pipes — no ACL, no FILE_FLAG_FIRST_PIPE_INSTANCE — plus HTTP/WS Yes L2 Named-pipe IPC + HTTP/WS + push notifications No nesting evidence. What exists is a windows-latest PTY test matrix over shells (cmd.exe, bash --noprofile --norc, pwsh), not agents HTTP/WS + IPC Untested Yes
qscreen Rust MIT 6 2026-07-21 Named pipes, .first_pipe_instance(true), per-user namespacing Yes L2 qscn CLI + ScreenFrame/ScreenRun structured protocol + AttachMode::Bytes raw attach Untested qscn verbs Untested Yes
quil Go MIT 11 2026-08-01 Naked AF_UNIX on all platformsos.Chmod(path, 0600) is a no-op on Windows, no SO_PEERCRED; protection is the parent directory ACL only ConPTY yes; CI is ubuntu-latest only, 25+ Windows source files, zero Windows CI, zero external issues ever L2 MCP (18 tools) + quild CLI + TUI Mouse-mode forwarding for nested alt-screen apps (internal/daemon/mousemode.go) MCP, 18 tools — the richest agent-facing surface Untested Yes
wezterm-mux-server Rust MIT 28,101 (WezTerm) 2026-07-31 AF_UNIX via the uds_windows shim Yes L2 (--daemonize, handled explicitly on Windows) 15 CLI verbs (list, spawn, split-pane, send-text, get-text, kill-pane, proxy…) + versioned varint binary codec + a tmux -CC client Untested nested; attach/detach is first-class 15 verbs, headless by design Untested Ships in the Windows zip and Inno installer — but the newest tagged release is 2024-02-03; the rolling nightly tag is the only current Windows path, so "pin a version" and "run on Windows" are in direct tension
OpenCode /pty TS/Bun MIT 192,203 2026-08-02 HTTP + WebSocket Yes (useConptyDll: true on win32) L1+ — cursor-resumable replay, but it dies with opencode serve. Not L2 REST + WS + replay cursor + ticket auth N/A — network client REST + WS N/A — the transport is the console-independence Yes (opencode-windows-x64.zip), but the /pty API is documented nowhere on opencode.ai and can change without notice
ao pty-host Go Apache-2.0 8,742 2026-08-02 Loopback TCP, 8-message binary protocol [type][BE32 len][payload] Yes L2, explicitlyCREATE_NEW_PROCESS_GROUP + DETACHED_PROCESS "so the host survives daemon exit"; replays scrollback to new clients; stays alive on PTY exit, mirroring tmux. Resize arbitration: applyLargestLocked sizes the shared PTY to the largest attached client, the only such rule found Binary wire protocol: GetOutput(lines), Status, Kill, SendMessage Untested nested — it is a network-attached design, so the prefix and alt-screen questions do not arise, but nothing has been run inside a second ConPTY Those four verbs N/A — network client No. Vendor the package or run the AO binary; Apache-2.0 makes that legally clean. It is a Hidden: true cobra subcommand — an internal contract
upterm Go Apache-2.0 1,270 2026-07-25 SSH, public-key auth (--authorized-keys, --github-user, --gitlab-user) Host yes (charmbracelet/x/conpty + Job Objects); relay uptermd is Linux-only No — L1 only. The session lives exactly as long as upterm host. No detach, no reattach SSH Deliberately ignores os.Interrupt "to prevent upterm from dying when SSH clients send Ctrl+C to child processes via ConPTY" SSH N/A — SSH is the transport Yes (scoop)
No-PTY architecture (claude -p stream-json + hooks + OTel) Vendor CLI Vendor-controlled, publicly documented Continuous Ordinary pipes; HTTP for hooks and OTel Yes — but "identical on Windows and Linux" is inference, never measured on both N/A — you own the process Bidirectional NDJSON + 5 hook types (incl. http and mcp_tool) + 34 typed OTel identifiers + JSONL transcript tree. Per agent vendor — this is an architecture you implement once per CLI, not a binary you install, so it forfeits agent-agnosticism No PTY, so no nesting problem at all Near-complete: NDJSON, permissionDecision, updatedInput, typed telemetry Yes, by construction — the only row that passes this column outright N/A — it is the agent CLI's own flag surface

Comparison table: the PTY-as-a-service lineage

These expose a terminal over HTTP, WebSocket or SSH. Most are refuted, and each is refuted by a single identifiable cell — which is what makes them worth reading: the failure modes repeat.

Name Language Licence Stars Last activity Interface Native Windows Persistence The deciding cell
OpenCode /pty TS/Bun MIT 192,203 2026-08-02 REST + WS + replay cursor + ticket auth Yes L1+ Best-shaped attach API in the census — undocumented contract, and a UTF-8-string wire with a fatal decoder and 64 KiB UTF-16 chunking, so it is not byte-exact
ttyd C MIT 12,113 code 2026-03-20 HTTP + raw WS Source yes, shipped binary broken L0 src/protocol.c:377-383a new process per WebSocket, unconditionally killed on close. The only Windows release (MinGW 1.7.7, 2024-03-30) fails to spawn any child on Windows 11 build 26200, your exact build. MSVC fix merged 2026-03-19; no release cut. WinGet and Scoop both pin the broken 1.7.7
gotty (yudai / sorenisanerd) Go MIT 19,531 / 2,523 2024-08-01 / 2026-08-01 HTTP + WS No L0 creack/pty start_windows.go is literally return nil, ErrUnsupported; the active fork ships freebsd/netbsd/openbsd/solaris and deliberately omits windows
wetty Node MIT 5,362 2026-07-31 HTTP + socket.io No L0 Every PTY, including --command, is pty.spawn('/usr/bin/env', …); it parses GNU coreutils version strings and gates local mode on process.getuid?.() === 0, undefined on Windows
terminado / jupyter_server_terminals Python BSD-3 373 / 20 2025-08-02 (docs; last code 2024-04-30) / 2026-01-14 REST + WS ["stdout",text] Yes, via pywinpty L1 (NamedTermManager), L0 (UniqueTermManager) A design template, not a product. Its platform selection is a nested try/except ImportError chain, not a platform conditional — if both imports fail it binds PtyProcessUnicode = object and fails later with an obscure AttributeError. pywinpty underneath binds a per-PTY ephemeral 127.0.0.1 TCP listener with an accept() race, a str round-trip and a literal '0011Ignore' sentinel
sshx Rust MIT 7,567 2025-06-19 (13.5 months) gRPC client↔relay + custom canvas UI Yes (zhiburt conpty) L1 The author's own source comment: "I can't get powershell.exe to work with ConPTY, since it returns error 8009001d." No arbitrary-command argument, no detach, self-hosting "not supported at the moment", and every release has zero assets
tmate C (tmux fork) BSD 6,092 2026-07-29 SSH No L2 A tmux fork, so it inherits tmux's POSIX dependency. The project is alive — the "shutting down" claim traces to a downstream README rather than tmate itself and was not re-verified; the pushed_at date is the actual evidence
Coder agent/reconnectingpty Go AGPL-3.0 + proprietary enterprise/ 14,004 2026-08-02 Coordinator mesh → WS/SSH Yes L1, self-terminating Prior art only. Its screen backend is gated runtime.GOOS == "linux"; its buffered backend is a 64 KiB ring dumped raw and self-labelled "(buggy)"; it closes itself after Options.Timeout (default 5 min) and needs a coderd control plane plus Postgres
VS Code ptyHost TS MIT Active VS Code internal IPC Yes L1 across window reloads The most complete prior art in the census, and not the only implementation of the primitive — Eclipse Theia has an independent attach(id) with a Windows-handling shell process. PersistentTerminalProcess with detachFromProcess(id, forcePersist), shouldPersistTerminal, an orphan-question protocol and serializeTerminalState(ids). Replay is not a raw byte dump — it runs @xterm/headless plus @xterm/addon-serialize and records resize events too, up to a 10 MB MaxRecorderDataSize. Contrast Coder's 64 KiB raw ring. Open-source server packaging (openvscode-server) is Linux-only
Eclipse Theia TS EPL-2.0 21,617 2026-08-01 HTTP/WS or Electron IPC Partial attach-by-id exists A second, independent implementation of the same primitive: base-terminal-server.ts:49 async attach(id: number) against a backend terminal registry, and packages/terminal/src/node/shell-process.ts:90-91 is if (isWindows) { return 'cmd.exe'; }. Unvalidated on server-mode packaging — nobody checked whether the backend runs headlessly on native Windows, which is Q25 and the only thing between "reference" and "option"
andyk/ht Rust Apache-2.0 902 2025-07-25 HTTP + WS + NDJSON over stdio No Its own description is nearly the brief verbatim — "wrap any binary with a terminal interface for easy programmatic access" — and it embeds avt so callers query the rendered screen, not the byte stream. src/pty.rs uses forkpty + nix + tokio::io::unix::AsyncFd. Refuted on the native-Windows constraint, decided by that one file — so if its description is what you wanted, what you want is a Windows reimplementation of it, not a port

Comparison table: ancestors and POSIX-only tools

Kept short deliberately. Each row is here so nobody re-searches it, and each one is out on the native-Windows constraint, decided by a single cell.

Name Language Licence Stars Last activity The deciding cell
GNU screen C GPL 4.9.1 (2023) Ancestor. Its own original beta-test README: "Since 'screen' uses pseudo-ttys, the select system call, and UNIX-domain sockets, it will not run under a system that does not include these features of 4.2 and 4.3 BSD UNIX"
tmux C ISC 3.7b (2026-07-01) Ancestor, and the contract everyone clones. POSIX-only; cmd-attach-session.c:74-76 holds the nested-session refusal string
dtach C public-domain-ish 732 2025-06-20 README: "assumes that the host system uses POSIX termios, and has a working forkpty function available." Also the purest opaque relay in the census — no terminal emulation layer at all
abduco + dvtm C ISC 969 / 956 last commit 2020-04-30 / 2021-03-06 (not pushed_at, which is later for both) POSIX and dormant. Still the closest precedent for the split you want — abduco's author: "these are two distinct features: window and session management shouldn't be intermingled"
coder/boo Zig MIT 748 2026 (bare year, not re-pulled) POSIX — std.posix in 11 files, zero AF_UNIX hits. Notable for swapping screen's ancient emulator for libghostty-vt and shipping peek --json / wait --idle as first-class verbs
tuios Go Unix-socket-only daemon, zero Windows build tags, procfs dependency
claude-squad Go AGPL-3.0 8,222 2026-07-30 go.mod has creack/pty v1.1.24 and no ConPTY library of any kind, despite active maintenance; hard tmux prerequisite
uzi Go MIT 580 2025-06-04 Hard tmux prerequisite; dormant; zero Windows issues ever filed — neglect, not confirmed non-support
reptyr C MIT 6,309 2025-11-20 — 8+ months dormant The POSIX answer to the structurally identical problem, via ptrace. No Windows analogue exists, after 10+ years
Roost Swift+AppKit / Rust+gtk4 unstated 2026 (bare year) Its own docs: "Not a Windows app — macOS and Linux only"
wtmux; amux; pymux; conmux; atch; diss; retach; monomux; kip mixed mixed small mixed One cell each: detach/attach "planned"; darwin+linux release artifacts only; unmaintained since 2023; abandoned 2019 with no session support; POSIX sockets (four of them); an in-process TUI rather than a daemon

How to re-check any cell yourself. The placement procedure is mechanical, and the transport row is usually the one that decides. Grep the source for std::os::unix::net, std.posix, forkpty or nix with term features (POSIX-gated); for \\.\pipe\, CreateNamedPipeW, interprocess::GenericNamespaced or tokio::net::windows::named_pipe (named pipes); for TcpListener::bind("127.0.0.1:0") (loopback TCP); or for an unconditional net.Listen("unix", …) (naked AF_UNIX). One caveat that invalidates the obvious shortcut: AF_UNIX is a real Windows transport since Windows 10 1803, so a UnixListener in source proves nothing about platform support either way. And for Go projects, creack/pty alone is not a disqualifier — quil depends on it and on charmbracelet/x/conpty and works on Windows. The correct test is the compound one: creack/pty present and no ConPTY library of any kind.

What this does not solve

This is the section to read twice. Every option above shares a set of limits, and most of them are not properties of the tool you pick — they are properties of the platform, the topology, or the machine you are sitting at.

Containers relocate the problem; they never replace the middle layer

The tempting move is "put the agent in a container and let the container be the persistence boundary." It does not work, and the reason is structural: containerising an agent session relocates PTY ownership, it does not eliminate it. Every viable container path still needs something inside the boundary to own the session — tmux-in-pod, or a proprietary attach layer — and it adds one more VT-parsing and resize boundary on top of the ADE's own PTY. Containers are an optional outer isolation shell around the middle layer, never a substitute for it.

The WSL question resolves more narrowly than the folklore, and to exactly two surviving paths. Hyper-V-the-hypervisor and WSL2-the-compatibility-layer are different code paths over the same primitives, so "containers on Windows means WSL" is false. But after applying the open-source constraint, exactly two open-source non-WSL container paths survive on native Windows: Podman with --provider hyperv, and minikube --driver=hyperv. Podman's own Windows tutorial states the constraints plainly: "Because Podman uses WSLv2 or Hyper-V recent features, you need Windows 11 or later", Hyper-V Administrators membership is required (mitigable once via podman system hyperv-prep), and "WSL and Hyper-V machines cannot run simultaneously". minikube's driver page lists "Hyper-V — VM (preferred)" for Windows — first-class, not a fallback — though note the same list also marks the Docker driver preferred, so the honest reading is that Hyper-V is not a second-class option, not that it is uniquely blessed.

Everything else fails on one of four grounds.

Closed source. Docker Desktop is proprietary and paid above 250 employees or $10M, and its own docs say docker-users membership "is equivalent to granting administrative privileges on the host" — while the widely-repeated "Hyper-V backend is deprecated" claim is false per that same page. Docker Sandboxes sbx is architecturally the closest existing thing to what you want and is binaries-only (docker/sbx-releases publishes no source). Two things on that page are verbatim and load-bearing: "Sandboxes persist after the agent exits" and "Docker Desktop is not required to use sbx". Two things often quoted alongside them are not on it and are withdrawn here: there is no sbx create command on the get-started page — the invocation shown is sbx run --name my-sandbox claude, with the agent as an argument to run — and the phrase "re-attaches from anywhere" does not appear. The absence of a Windows edition gate is likewise not evidenced there. Also closed: Daytona, which closed-sourced in June 2026 — its repository root now contains only README.md and an assets folder, GET /license returns 404, and its README states "As of June 2026, Daytona's core development has moved to a private codebase", leaving 72k stars on an empty repo.

Hard WSL. Rancher Desktop requires WSL unconditionally — that requirement is on its Windows installation prerequisites page rather than on the docs root, which is an introduction page that does not mention WSL at all; cite the prerequisites page, not the root, and re-read it before planning around it, because this guide's own record of the exact sentence traces only to the root. k3s is Linux-only by its own requirements page. Claude Code's own Bash sandbox says verbatim: "This option does not support native Windows. On Windows hosts, use WSL2 or one of the container or VM approaches below". And OpenHands states in its local-setup documentation that "OpenHands only supports Windows via WSL… Native Windows is not officially supported", superseding an older non-WSL guide.

Dead. DevPod's last release is v0.7.0-alpha.34 from 2025-06-23, and issue #1946 is where the abandonment is usually sourced. Be careful with that citation, because it is the one place in this guide where the evidence tier inverts if you are sloppy: the sentences "their attention is focused on vcluster" and "they do not have plans at this moment to allocate resources to maintaining this project" are from the issue body, written by skevetter — a third party with no repository association, proposing his own fork — and his own framing is explicitly hedged: "based on the lack of engagement from the original developers…, it is fairly safe to say they do not have plans at this moment…". That is a community member's declared guess, not a maintainer statement, and it should never be quoted as the project's own words. The unhedged evidence is the release date. Also dead: dagger/container-use, whose own open PR says "#242 added Windows compilation but no installation method", whose Windows terminal path discards caller arguments, and which registers as a stdio MCP server — a shape that cannot own persistent sessions at all.

Wrong kernel. Windows Containers, in both process and Hyper-V isolation, run a Windows guest and cannot run Linux binaries. Hyper-V isolation gives you a real VM boundary and the guest inside it is still Windows, which is the whole problem.

And two container-adjacent notes worth keeping, because both save time. kubectl attach is L0 — a fresh process every time — and the community's own fix for that is tmux inside the pod, via tools like predatorray/kubectl-tmux-exec, which need bash and tmux on the client and therefore cannot run on native Windows at all. That is this section's thesis in miniature: the container did not remove the need for a session layer, it moved the session layer inside the container and then required a POSIX client to reach it. Separately, do not count kind and Docker Desktop's "bundled Kubernetes" as two options — Docker Desktop's own documentation says the bundled cluster is kind (or kubeadm), so they are one candidate wearing two names, and kind inherits whatever backend you point it at rather than providing one.

One container-adjacent tool deserves a specific warning because it looks like a fit and is architecturally hostile. anthropic-experimental/sandbox-runtime launches the sandboxed process via CreateProcessWithLogonW under a different local user account, "not as the calling user". Its README states the consequence without any inference needed: "Per-user tool installs are not reachable… tools installed under your profile (nvm/fnm-managed Node, per-user winget/Scoop packages, pip install --user, %LOCALAPPDATA%\Programs\…) resolve on the inherited PATH but cannot be opened by the sandbox account." Claude Code's default Windows install lands exactly there. The second consequence — that a console owned by a foreign user SID is not AttachConsole-able by your process — is an inference from documented per-session console semantics and has never been tested.

Late attach cannot be made into a foundation

If you were hoping to reach into an agent process that is already running under someone else's PTY, that door does not open. The full evidence is under AttachConsole and Who owns the session above; what belongs in a limits section is the shape of it, in four lines.

  • Microsoft has told developers not to build on the write half. WriteConsoleInput carries an Important banner calling it functionality "no longer a part of our ecosystem roadmap" and a separate Tip calling it "the wrong-way verb for this buffer"; the classic-vs-VT page names input injection and output scraping as "a vector to cross security and privilege-levels or domains".
  • The read half is gated by integrity level, directionally. A lower-integrity process cannot read a higher-integrity console. Same-user, same-integrity siblings — which ORCA's children are — are unaffected; it bites for elevated agents, cross-user, and session 0.
  • Nobody has built a general late-attach library, and the reason is that every mature implementation ships exactly one half: NVDA reads and never writes, winpty and wexpect read and write but own the console from birth. The halves never combine.
  • It is not a nested topology at all, so it is not an alternative to anything else in this guide. It is an out-of-band, sideways-reaching daemon — a different shape, worth naming rather than silently conflating with the others.

Whether AttachConsole even reaches an ORCA-spawned agent in the exact Electron-plus-node-pty topology is untested (Q5). A success there buys you a read-only out-of-band inspection path and nothing more.

The node-pty defects that live in your stack today

These are not risks you might take on. If you run ORCA, they are already in your process. All of them are microsoft/node-pty issues.

Defect State Consequence
AttachConsole failed crash when killing an already-exited shell PR #886 open since 2026-02-06, unmerged; PR #901 (Node 22 resize-after-exit) open, unmerged Surfaced as user-visible crash dialogs in shipping Electron products — openai/codex#25415 and microsoft/vscode#201029
The partial mitigation that did land Commit 2b25c761, "Don't get process list on kill if not yet connected", in v1.2.0-beta.11, 2026-02-03. This is not PR #886's try/catch guard — it predates #886 by three days and does something different; do not read it as "#886 landed" Codex and VS Code shipped stale bundled copies and crashed for want of this one commit. The fix on your side is one line of dependency hygiene: pin node-pty ≥ 1.2.0-beta.11
Root cause still open vscode#201029 final comment: "kill() unconditionally tries to enumerate console processes for a shell whose exit has already been handled" — resize() has an _exitCode guard, kill() does not Triggered by short-lived diagnostic shells (cmd.exe /c "exit /b 0") racing the kill path
Data race in ptyHandles issue #921, fixed 2026-05-13 A process-global vector mutated from per-PTY watcher threads; concurrent std::remove_if dereferenced a moved-from unique_ptr. A real hazard at moderate session counts — which is exactly what one-agent-per-worktree produces
Conout worker deadlock PR #943, merged 2026-07-31 A regression of a bug already fixed once (#763 → PR #885 → regression → #943). Triggered when the Node inspector pauses the conout worker thread
ORCA's own instance stablyai/orca#9586, closed with zero comments and no maintainer reply Read the "root cause / fixed in v1.2.0-beta.11" text there as the reporter's own unreviewed self-diagnosis, not an established finding

There is a second, quieter hazard in the same library, set out under The four things a middle layer must get right on Windows: node-pty's bundled ConPTY prototypes have drifted from the current ConPTY ABI, in both directions, on the useConptyDll path. The symptom of a mismatch like that is a clear or a resize that silently does nothing, which is the worst class of bug to debug, and it lands on you rather than on the library.

And one supply-chain note for anyone reaching for a Go PTY library to escape all this. creack/pty has no Windows backend, so Go projects that need ConPTY have to route around it. Wave Terminal's route is a go.mod line: replace github.com/creack/pty => github.com/photostorm/pty. That fork has 15 stars and was last pushed 2024-04-14. It works, and it is a named supply-chain risk sitting in the dependency graph of a 21k-star product. If you find yourself about to add the same replace directive, know what you are adding. The better-supported answers are charmbracelet/x/conpty (used by upterm and quil), aymanbagabas/go-pty (used by Agent Orchestrator) and UserExistsError/conpty (used by kandev).

What each non-multiplexer route gives up

If you decide against a PTY-owning daemon, know precisely what you are trading. Each row below is a real route somebody ships; each forfeits something specific.

Route Persistence Reattach What it costs you
Windows OpenSSH Server No No Win32-OpenSSH#2291, filed by a Microsoft member and still open: no tmux equivalent exists. sshd puts children in a Job Object and disconnect tears it down — not SIGHUP, so there is no signal to trap. Caveat worth carrying: the Job-Object mechanism rests on one maintainer sentence, and code searches for CreateJobObject/KILL_ON_JOB_CLOSE in openssh-portable return zero hits. Treat it as absence of evidence, not as established
Windows Service Yes No Plus a session-0 problem in two halves. The established half: a SYSTEM account has no user-profile agent credentials, and agent CLIs authenticate from the profile (~/.codex/auth.json, ~/.claude/). The inferred half: console objects are per-session and AttachConsole takes a PID, so it should not cross the session-0 boundary — an inference from two documented facts, never observed to fail
Scheduled Task Yes No Same, except it avoids the credentials half if you run it as the user rather than as SYSTEM
Hooks + file/JSONL IPC Structured replay Not live Resumed sessions replay saved hook output rather than re-running it, so timestamps and commit SHAs go stale
LangChain agent-inbox Yes, as a queue No — a queue of pending decisions, not a session The pattern is transferable and the implementation is not: it requires from langgraph.types import interrupt and a running LangGraph deployment, so you adopt a framework rather than a component. (Its response discriminator was recorded as 'response' rather than 'respond', with the config flags using allow_respond — an API detail carried from one pass with no file or doc URL, never re-verified)
Job-queue fleets (Actions runner, Buildkite, OpenHands) Yes, at scale No — a streamed log, not a shell A deliberate trade, and the right one if you never attach
RDP / tscon Yes Yes The in-box counterexample that breaks "every route gives up one" — a disconnected RDP session keeps every process running with a live console and you reattach by reconnecting. The cost is whole-session granularity: no per-worktree agent tabs, you can't connect to the console session, and client SKUs allow one interactive session
codex app-server --listen ws:// Yes A standing listener the client does not own The other counterexample — but its own README labels websocket "experimental / unsupported. Do not rely on it for production workloads", the daemon lifecycle manager is explicitly Unix-only, and it owns no PTY

The cleanest evidence that file-based IPC narrows PTY dependency without eliminating it comes from a harness author in openai/codex#11750 (still open), who describes the workaround as "spawning codex fork inside a pseudo-TTY, polling the filesystem for new rollout files to discover the forked session ID, killing the TUI, and then using codex exec resume --json <forked_id> for the actual query. This works but is fragile and slow (~6s overhead for TUI startup)." Further down the same issue he adds that it "requires PTY bindings (e.g. Python's pty module or node-pty), and is tightly coupled to the rollout file format" — two separate sentences from two places in the issue, which is worth knowing because they are usually spliced into one quotation that appears nowhere in the source. Read the substance carefully before generalising from it: thread/fork already exists on app-server, so the PTY fallback exists because codex exec lacks a fork verb, not because the capability is intrinsically PTY-bound. (Also note the project that filed it, bearlyai/OpenADE, has no LICENSE file and license: null — all-rights-reserved by default, so it is usable as evidence and not as a component.)

Unauthenticated local IPC: the shared security problem

Several of the strongest candidates share one defect, and it matters more the more you share the machine. The transport ranking — ACL'd named pipe, then AF_UNIX, then loopback TCP — and the per-user rather than per-process ceiling on all of them are stated as build requirements under the verb set. Here is the evidence, in descending order of how much it should worry you:

  • psmux's cross-session port is unauthenticated and full-duplex. src/cross_session_server.rs binds a loopback port, accepts the first connection with no handshake, registers it as a tee writer so it receives every byte the pane's ConPTY emits, and writes everything it reads back into the PTY — exfiltration and injection, gated only on the user performing a cross-session pane move, and reachable in practice or not is Q6. The line numbers are in the psmux entry above, along with the separate weakness in the main control listener's AUTH key: a 64-bit non-cryptographic secret in a file with no explicit ACL.
  • oly's named pipe has no ACL at all, and no FILE_FLAG_FIRST_PIPE_INSTANCE, so a hostile local process can pre-create it and harvest client connections. Microsoft's CreateNamedPipe documentation states that a NULL security descriptor grants "read access to members of the Everyone group and the anonymous account" — for a pipe carrying agent terminal I/O, that is an API-key disclosure path, covering whatever the agent prints and whatever you type into it. The project's own roadmap has listed the fix as not started since 2026-03-12; the evidence, and the later audit that fixed the web layer without touching this, are in the oly entry above (Q13).
  • ao pty-host says so itself. host_main.go carries the caveat in the source: "loopback bind only; any local process on this host can connect to the assigned port. A per-session random token handshake is the upgrade path."

The counterexample, and the design to copy: rmux is the only candidate that gets both halves right — a pipe name computed from the caller's SID and integrity level rather than picked from a string, plus FILE_FLAG_FIRST_PIPE_INSTANCE and an ImpersonateNamedPipeClient peer check. Namespacing prevents accidents; ACLs prevent attacks. You need both.

If you homegrow a shared-secret scheme instead, read Erlang's own distribution security section first: cookie authentication is "not cryptographically secure" and communication is cleartext by default. Do not build something weaker than a scheme whose own documentation disclaims it.

Limits that live in the platform, not in any candidate

The mechanics of these are under Nesting; collected here so a limits-only reader still meets them, with the consequence rather than the argument.

  • ConPTY reflow/resize desync is unfixable from outsidemicrosoft/terminal#15976, a megathread, with the in-process ConPTY spec calling it unsolvable in the current architecture. Consequence: a nested middle layer that keeps its own screen model becomes a third independent representation of the same screen. Whether the in-process redesign has shipped, partly shipped or stalled is Q17.
  • Passthrough fixed double VT parsing and made one thing worse. ConPTY has passed application VT output through unmodified by default since PR #17510 merged 2024-08-01, so "double VT parsing is unsolved at the OS level" is wrong today. But passthrough injects VT sequences into stdout regardless of parser state, which is the mechanism behind microsoft/terminal#19621 — still open, filed by a Microsoft member. If your interface is tmux control mode, that issue is your issue.
  • Pasting more than 5 KiB into a slow-reading app deadlocks the whole terminal, and Ctrl-C stops working — microsoft/terminal#17384, open since 2024-06-06.
  • Residual VT-passthrough issues remain open in microsoft/terminal#17643. Cooked-read reflow, ScrollConsoleScreenBuffer, the DA3 truncation and the PSReadLine SGR issue are all fixed — do not carry those forward as hazards.
  • OSC 133 cannot be forwarded through a nested layer. tmux's maintainer nicm gives the reason across two separate comments in tmux#5237 (open) — quoted here as two, because they are usually run together under an ellipsis that implies one utterance. First: "If tmux writes a prompt at 10,10 and you drag the pane so it is now at 15,15, how does tmux tell the terminal it has moved?" And later: "The sequences are not powerful enough… they are meant for shells, not full screen programs." A nested layer must parse-and-re-expose, not forward. And Claude Code does not emit OSC 133 anyway — at least five duplicate requests since May 2025, all bot-closed. (If you want OSC 133 from a shell rather than from an agent, note that even cmd.exe has a path: oh-my-posh's shell_integration documentation states it "Works in bash, cmd (Clink v1.14.25+), fish, powershell and zsh", so "no cmd.exe OSC 133" is false. tmux's own OSC 133 eventspane-command-started and friends — are in unreleased master only; the latest release is 3.7b.)

The standards and guarantees that do not exist at all — no MCP terminal convention, no specification of tmux control mode independent of tmux, no Windows reptyr, no ConPTY compatibility matrix, no primary source for the build thresholds — are collected under What does not exist, so that a weekend is not spent searching for any of them.


The playbook: what to pick and how to verify

The decision that matters most, first

Before you compare daemons, answer this: do you need the byte stream of a program that insists on a terminal, or a human typing into a live session — or neither?

Everything downstream turns on it, because those are the only two things PTY ownership buys that no other channel provides. (Not "exact on-screen state" — screen state is derivable from any byte stream plus a VT emulator, as asciinema's relay demonstrates without ever touching a PTY. It is the byte stream that needs the PTY.) If you need neither, you do not need any of the candidates in this guide, and every hard problem in it disappears at once: no ConPTY, no nesting, no prefix keys, no resize propagation, no double parse. The channel that replaces them — claude -p stream-json, hooks as the write path, OpenTelemetry as the read path — is set out in full in When you need a session daemon.

Answer it by measurement, not by intuition. Instrument one week of real work and count the sessions where you actually attached and typed mid-run against the sessions you only observed. That is Q9 below and it costs nothing but a week of passive logging — a "sufficient" answer moots most of the candidate comparison, and a "not sufficient" answer gives you a number for how often you need attach, which is the input to every remaining trade-off.

The running order, with wall-clock

Q9 is the highest-leverage question and it is not the first thing to do, because it takes a week and two other tests take two hours each and can kill whole branches before the week is out. Start all three on day one:

When What Cost What it can kill
Day 1, morning Q23 — do any candidate's verbs work with no console attached? Step 3 of the verification ritual, run once against every candidate at the same time 2 hours The entire PTY branch, in one afternoon. If the verbs need a terminal, every candidate here is a multiplexer rather than a middle layer, and the no-PTY route wins by default. This is the cheapest disqualifier in this document
Day 1, afternoon Q1a — can agentCmdOverrides produce a raw-pipe child at all? Read ORCA's spawn path end to end 2 hours tmux -CC as an interface, which moots the whole -CC corruption question (Q1) and drops psmux to its plain verb surface
Day 1, and then leave it running Q9 — is the no-PTY architecture sufficient for your actual sessions? Passive logging, nothing to babysit 1 week elapsed, ~0 attention Most of the candidate comparison, if the answer is "sufficient"
Week 2, once Q23 and Q1a are in Q3 (Zellij input path, 1 hour), Q4 (xterm.js CSI 18 t, 1 hour), Q12 (OpenCode /pty on Windows, 1 hour), then Q18 (nesting, 1 day) ~1.5 days Individual candidates, not branches

The logic is simply cost-versus-blast-radius: two hours that can eliminate eleven candidates beats a week that can eliminate eleven candidates, and you can have both because the week is passive.

Run your own constraints as a filter first

Before any of the situational sections below, run this ladder. Each rung is a yes/no about you, not about a tool, and each one names what it removes. It takes about five minutes and it is the thing that turns "here is the whole space" into a shortlist.

  1. Native Windows, no WSL? → the entire POSIX table goes: dtach, abduco+dvtm, boo, ht, tuios, reptyr, tmate, gotty, wetty, claude-squad, uzi, mobydeck/atch and the rest. Also out: every hard-WSL container path. Survivors: psmux, Zellij, rmux, herdr, oly, qscreen, quil, wezterm-mux-server, OpenCode /pty, ao pty-host, upterm, plus the no-PTY route.
  2. Must it survive the ADE, not just the client?upterm goes (L1 only, no detach, no reattach) and OpenCode /pty goes as a middle layer (L1+, dies with opencode serve) — though OpenCode's cursor protocol stays as a design to copy. Survivors: psmux, Zellij, rmux, herdr, oly, qscreen, quil, wezterm-mux-server, ao pty-host.
  3. Do the verbs work with no console attached? → this is Q23 and it is not a preference, it is a measurement you have not made yet. Either nothing goes or everything PTY-owning goes, which is why it is first in the running order above. The no-PTY route and the two network-attached candidates (ao pty-host, OpenCode /pty) pass by construction.
  4. Do you need a pinned stable release?herdr goes (Windows ships preview-channel only; stable v0.7.5 has no Windows asset) and wezterm-mux-server goes (newest tagged release 2024-02-03; the rolling nightly is the only current Windows path). Survivors: psmux, Zellij, rmux, oly, qscreen, quil.
  5. Do you share the machine, or run anything you do not trust as your own user?psmux goes until Q6 is answered (unauthenticated full-duplex cross-session port) and oly goes until Q13 is answered (named pipe with no ACL). Survivors: Zellij, rmux, qscreen, quil.
  6. Does it have to behave identically on Linux?psmux goes outright (Windows-only CI and Windows-only release assets) and oly's parity is disproven by its Windows-specific expected-output fixture. Survivors: Zellij, rmux, and — if you relax rung 4 — wezterm-mux-server and herdr.
  7. Are you willing to vendor and build source rather than install a binary? → if yes, ao pty-host re-enters (Apache-2.0, the closest architectural match found) and so does WezTerm's varint codec as a protocol to implement. If no, both stay out.

Where the ladder typically lands. All rungs applied strictly, with rung 3 passing: Zellij or rmux, and the tiebreak is in the Linux-parity section below. Rung 6 relaxed: psmux re-enters with the strongest interface and the worst platform breadth. Rung 3 failing: nothing PTY-owning survives and you are on the no-PTY route whether you wanted to be or not. That is a decision procedure rather than a verdict — the ranking is yours, produced by your own answers.

If you only need to observe an agent

Take the no-PTY route and stop reading the candidate list. It is the only option in this guide that passes the console-independence test by construction — ordinary pipes, no console needed anywhere — rather than "probably, untested".

Two sentences of honesty about what that costs, because they are the reason this is not simply the answer for everyone. This is an architecture you implement per agent vendor, not a binary you install: claude -p stream-json, Claude Code hooks and the claude_code.* telemetry are Claude Code's, and Codex's exec --json plus rollout JSONL is a separate implementation of the same shape rather than the same one. You buy console-independence and forfeit agent-agnosticism — which is precisely the property "a layer any ADE can spawn with any agent CLI inside it" was asking for. If your agent roster is one CLI, the trade is excellent. If it is three, you are writing three integrations and tracking three config surfaces that "change under you", as the agent-state axis puts it.

Build it out of four vendor-maintained channels. claude -p stream-json for the duplex channel, with --include-hook-events, --forward-subagent-text (which carries parent_tool_use_id), --include-partial-messages and --replay-user-messages as companions — being honest that this is print/headless mode and it does not attach to an existing interactive TUI; it is a way to spawn the agent with no PTY at all. HTTP hooks for the write path, so the middle layer is the endpoint rather than a spawned child. OpenTelemetry for liveness, remembering that the master switch is CLAUDE_CODE_ENABLE_TELEMETRY=1 and not an OTEL_* variable, that content is redacted until you opt in per field, and that Claude Code strips OTEL_* from every subprocess it spawns so the config does not reach grandchildren. And the JSONL transcripts for replay — a tree rather than a file, with subagent fan-out readable from the filesystem alone, and undocumented enough that anything treating it as durable needs its own retention policy. Each of those four, with its flags, environment variables and documented limits, is in the opening section.

One load-bearing claim here is inference, not observation. That claude -p --output-format stream-json behaves identically on Windows and Linux is inferred from unqualified flag documentation over ordinary pipes. Nobody has run it on both platforms and compared the output. If cross-platform parity is what sold you on this route, spend an afternoon proving it before you build on it.

If you want a second, vendor-native instance of the same architecture, Codex's exec --json plus rollout JSONL under ~/.codex/sessions/YYYY/MM/DD/rollout-*.jsonl with resume --last is the same shape from a different company. The cross-vendor convergence on that shape is itself evidence that this is a real architectural pattern rather than one company's idiosyncrasy.

If the session must survive a reboot

Then nothing in this guide gives you what you want, and you should design for reconstruction instead of persistence. The persistence ceiling on Windows among everything surveyed is L2 — a detached daemon that outlives whoever spawned it. The only L3 claim found anywhere is cmux (Swift + AppKit, GPL-3.0-or-later, 25,483 stars), whose FAQ says "the state survives a full computer restart… Agent sessions like Claude Code, Codex, and OpenCode come back too". It is macOS only, so it is not a fallback and not something you can even evaluate on your platform — and the quote comes from a FAQ read once with no URL recorded, so treat it as reported rather than verified. It is here as the one existence proof that somebody built L3 at all.

So build the restart path explicitly out of three pieces. A session registry on disk, so orphans are findable after a metadata loss — ao pty-host is the model, persisting ~/.ao/windows-pty-hosts.json with {sessionId, ptyHostPid, pipePath}. A conversation-state source that is not the PTY — the JSONL transcripts and codex resume --last both survive a reboot, subject to the retention caveat above. And a respawn step that re-creates the process and replays state into it, because the process itself is gone.

Do not reach for a Windows Service or a Scheduled Task to close the gap. They give you persistence and take away reattach, and the Service path additionally lands you in session 0, where a SYSTEM account has no user-profile agent credentials — which is the half of the session-0 problem that is documented rather than inferred. A Scheduled Task running as the user avoids the credentials problem, and still gives you no reattach.

If you need a real PTY owner

You are here if either of the two things PTY ownership buys is on your list — the byte stream of a program that insists on a terminal, or a human typing into a live session. Read this whole section even if you want only the first one and no human is ever going to attach, because the criteria below apply to both cases and the human-attach material is a sub-case at the end, not the frame.

Attach must be first-class, not an afterthought, and it must carry a resumable cursor. This is the criterion that matters most for a headless consumer, not just a human one: the middle layer's whole value is that a new client — your supervisor process, restarted after the ADE was upgraded — can pick up an existing session, and a replay that starts from zero every time is not that. The one primitive nothing else substitutes for is attach(id, {cursor}) → {replay, cursor, stream}. OpenCode's /pty protocol is the model for the cursor semantics only — copy its absolute-output-cursor control frame, not its wire type, because the payloads are JavaScript strings that split surrogate pairs at the 64 KiB replay boundary and silently drop invalid UTF-8 on the way in. It is also L1+, not L2: it dies with opencode serve.

Watch out for string-typed wires generally, not just that one. Agent CLIs emit heavy Unicode — the in Claude Code's own window title already broke quil's emulator — and a from_utf8_lossy or a String.slice in the middle of the live path corrupts it permanently, not cosmetically. On the strict test — what is the declared type of the message the server sends on every output tick? — three candidates carry bytes end to end: qscreen's AttachMode::Bytes, oly's ServerMessage::Data { data: Vec<u8> }, and quil's PaneOutputPayload { Data []byte }. psmux's -CC %output does not qualify on the strict test, despite being octal-escaped and byte-oriented in intent, because the ring drain runs String::from_utf8_lossy at src/server/mod.rs:1225 before the escaping — the escape faithfully encodes bytes that have already been damaged. Where the axes section places psmux -CC in the raw-live-path pole, that is design intent; here, where you are choosing something to run, take the strict reading.

The prefix key is the thinnest-evidenced part of this whole area, and it is a daily-annoyance problem rather than a correctness one — but for a headless consumer it is a correctness one. Every multiplexer candidate's rebindability is inferred from "it has a config file"; the only documented remapping mechanism found in the entire census belongs to docker attach --detach-keys, which is not a candidate. The one primary-source statement about prefixes on Windows is herdr's own beta page listing "Prefix input-source switching | unsupported". What you actually want is stronger than rebinding — no prefix at all on the headless path, with the prefix existing only when a human attaches — and no candidate is known to offer it. That is Q24, it costs three hours, and if the answer is no you inherit a keybinding negotiation with every ADE you ever swap to, which is the exact thing this project exists to avoid.

Sub-case: if a human genuinely has to be able to sit down at it. Everything above still applies, plus two more considerations. Keep one in-box option on the table: RDP with tscon gives both persistence and reattach natively, with no third-party code at all. It is whole-session granularity, so it cannot give you per-worktree agent tabs, but if what you need is "walk away and come back to a live desktop", it is already installed. And in the candidate list, first-class attach/detach verbs stop being a nice-to-have — wezterm-mux-server documents attach and detach as first-class operations, psmux and rmux inherit tmux's, and qscreen's raw-byte attach mode is the cleanest of the small designs.

If you need it to work identically on Linux

psmux is out. Its CI builds only x86_64/i686/aarch64-pc-windows-msvc; the ubuntu-latest/macos-latest job runs two POSIX shell scripts and is not a build, and release v3.3.7 ships six Windows-only assets. It has the strongest interface in the census and the worst platform breadth, and those are the same fact seen twice.

Treat oly's platform parity as disproven, not unknown. It ships tests/output-copilot.expected.windows alongside tests/output-copilot.expected — a Windows-specific expected output for the same input log. That is direct evidence its VT/log-rendering layer produces different output on Windows than on Unix.

Survivors: Zellij (original target platforms, plus a zellij-x86_64-pc-windows-msvc.zip and an MSI at v0.44.3), rmux, wezterm-mux-server, and herdr. Among those, the tiebreak is usually posed as maturity against transport design: Zellij has 34.6k stars, an MSI, and the best-verified Windows claim of anything surveyed; rmux has the only identity-scoped namespace and the only three-layer Windows security model, against a single-author bus factor (roughly 936 of ~1000 commits from one author, a figure recorded once and never re-verified) and a dual-licence pair that is unstated in repo metadata — read LICENSE-* at the tag you would vendor before relying on it.

But check whether that tiebreak is even live for you, because for a lot of readers it is not. rmux's identity-scoped pipe is protection against another local principal — a second account, a low-integrity process, something you did not start. If you are the only account on the box and your threat model does not include a hostile local process, the entire security column collapses to a tie, rmux's main claimed advantage over Zellij stops discriminating, and the choice reduces to maturity against transport-design taste plus whatever the nesting tests (Q18) turn up. Say which of those two situations you are in before you spend an afternoon comparing pipe ACLs — and note the ceiling that applies either way: authentication in this whole category is per-user, not per-process, so ORCA, every agent CLI, every MCP server and every npm postinstall script already run as you and can already reach any of these endpoints.

Two more filters land on top of that set. If you need a pinned stable release, herdr is out (Windows ships preview-channel only; stable v0.7.5 has no Windows asset) and wezterm-mux-server is out — its newest tagged release is 20240203-110809-5046fc22, published 2024-02-03, so "pin a version" and "run on Windows" are in direct tension; the rolling nightly tag is the only current Windows path. That leaves Zellij (msvc zip plus MSI) and rmux (winget/scoop/choco). If you are willing to vendor source, ao pty-host opens up — Apache-2.0, the closest architectural match found, detached ConPTY with scrollback replay and largest-client resize arbitration — and so does WezTerm's varint codec as a protocol to implement rather than a binary to run. That codec is the only wire format surveyed that was explicitly designed for version-skewed client/server pairs, which is the ADE-churn insulation you are shopping for, expressed in wire form.

If you are wrapping under ORCA specifically

Start with the hook itself, because everything in this section and step 4 of the ritual runs through it. agentCmdOverrides is declared in src/shared/types.ts and consumed at src/shared/tui-agent-launch-command.ts:30, where the lookup is const override = args.cmdOverrides[args.agent] — an object keyed by agent identifier, whose value replaces the launch command for that agent. So an override that puts psmux underneath Claude Code looks like this:

// ORCA settings — shape inferred from the consuming code, NOT from ORCA documentation.
// Verify the key spelling and the value's shape against your installed version before
// relying on it; the one thing verified at source is the lookup at
// src/shared/tui-agent-launch-command.ts:30.
{
  "agentCmdOverrides": {
    "claude": "psmux new-session -A -s orca-${worktree} claude"
  }
}

Three things to get right when you write your own. The agent key must match whatever ORCA calls that agent internally — read it off the same file rather than guessing. The override replaces the whole command, so anything ORCA would have appended (model flags, working directory arguments) is now yours to reproduce. And the middle layer must exec the agent, not fork-and-exit, or ORCA's pane will see its child terminate immediately. Whether your override string can produce a raw-pipe child rather than a ConPTY child is exactly Q1a, below.

Answer Q1a before you do anything else, because it can moot the question you were about to ask. ORCA spawns through node-pty, which means always a ConPTY. The documented remedy for tmux control mode corruption is "give the child raw pipes" (ssh -T user@host tmux -CC). So the question is not "does ConPTY corrupt -CC in my topology" — it is "can agentCmdOverrides produce a raw-pipe child at all?" Read ORCA's spawn path from src/shared/tui-agent-launch-command.ts:30 through to the node-pty call and check whether any override shape bypasses the PTY. Two hours. If the answer is no, -CC is unreachable in this topology whatever the corruption test would have shown, psmux drops to its plain CLI verbs, and the case for psmux drops to what its plain verb surface is worth. This is the general rule stated under Nesting: test whether the cure is reachable before testing whether the disease exists.

Be precise about what the corruption claim actually says, because it is easy to over-state. psmux's maintainer scopes it to running over SSH with a ConPTY console; the generalisation to a node-pty ConPTY is inference, not the source's claim, and Q1 tests it in an afternoon. A related divergence is documented rather than inferred and is worth knowing regardless of how Q1 turns out: %output data on Windows may differ from what a Unix tmux session produces, because ConPTY normalises line endings and processes some cursor movement internally.

Pin node-pty ≥ 1.2.0-beta.11 on ORCA's side. Commit 2b25c761 ("Don't get process list on kill if not yet connected") landed upstream on 2026-02-03 and is what stops the AttachConsole failed crash dialogs; the products that still crash are shipping stale bundled copies. Note this is a mitigation rather than the full fix — PR #886's try/catch guard is still open and unmerged, and the root cause in kill() is still open at vscode#201029. This is the cheapest thing on this entire list.

Check Zellij's input-path gate before concluding anything from a bad first run. Inside ORCA's node-pty ConPTY, neither TERM nor WT_SESSION is necessarily set, so Zellij takes its native-console INPUT_RECORD input path rather than the VT one. This is a config check, not a hazard — the details are in the Zellij entry above — and one environment variable in the override command (TERM=xterm-256color) forces the other path. What is genuinely unknown is what the symptom would be if the native-console path misbehaves under a nested ConPTY, which is why Q3 currently has no pass/fail criterion.

Assume nesting is untested, because it is. Double-ConPTY nesting has not been verified for any candidate, including oly, whose supposed nested-agent tests turn out to spawn shells rather than agents. Q18 is the test; budget a day for all four candidates and run it with a resize, an alt-screen app and a mouse-mode app.

If you use psmux's nesting guard, read the code and not the error message. The check at src/main.rs:954 and :4056 reads PSMUX_ALLOW_NESTING, but the error printed at :958/:4060 says "psmux: sessions should be nested with care, unset PSMUX_SESSION to force". A scripted wrapper that follows the message will not clear the guard.

If you share the machine with anyone

Then re-read the unauthenticated-IPC subsection above and treat it as a filter, not a footnote. Prefer a named pipe with a real ACL over AF_UNIX over loopback TCP, in that order. Ask Q13 (does oly's ACL gap still exist?) and Q6 (is psmux's cross-session path reachable?) before adoption, not after. And run Q22 — a single NVD/GHSA sweep for the four small candidates, one hour — because only their own issue trackers were ever checked.

If you end up building it yourself

Every candidate got most of it right and each forfeited exactly one of {maturity, Windows-nativeness, standalone packaging, contract stability}. If that trade is unacceptable, the two things that most often go wrong are not in the verb list, and both are cheap to get right at the start and expensive to retrofit: answering ConPTY's startup handshake, and not putting a string type on the wire. Adopt an existing framing rather than inventing one — tmux -CC is decade-hardened with at least five implementers, and WezTerm's varint codec is explicitly built for version skew (read its LICENSE.md at the commit you vendor; GitHub's licence API reports NOASSERTION for the repository). Use rmux's identity-scoped pipe shape for the namespace, and CREATE_NEW_PROCESS_GROUP | DETACHED_PROCESS plus an on-disk session registry for persistence.

And pick your VT parser deliberately, because the choice is narrower on Windows than it looks and the census made it five times. Every candidate needs a screen model for snapshots and scrollback even if the live path ships raw bytes, and here is what the field actually chose:

Parser Language Who uses it What you should know
vt100-psmux 0.16.2 Rust psmux and qscreen, independently — qscreen's copy is a clean cargo vendor of the published crate, not a fork of psmux's directory A screen model over Alacritty's vte 0.15; originally Jesse Luehrs' vt100-rust, patched and republished by marlocarlo. Two independent adoptions is the strongest track record in this table
vt100 (upstream) Rust oly The unpatched ancestor of the above
charmbracelet/x/vt Go quil Carries a known upstream spec-compliance defect around 0x9C in UTF-8, already worked around downstream — see trap 2
libghostty-vt Zig boo, Roost, ghostel The only one with a shipped native-Windows consumer (ghostel's src/ConPtyProcess.zig), and whether that means libghostty-vt itself builds on Windows or that ghostel routed around it is Q21, one hour of reading
asciinema/avt Rust asciinema's own relay Apache-2.0, 229 stars, and its only dependencies are rgb and unicode-width — no libc, no platform-conditional code anywhere. It should therefore compile on native Windows. That is an inference from the dependency list and nobody has built it, which makes it either the cleanest option here or an afternoon you lose finding out why not

The verification ritual

A success message from the tool proves nothing. "Session created", "daemon started", "attached" — none of these tells you whether the session survives the thing you care about surviving, whether the verbs work when no human is present, or whether the daemon is even a separate process. This is the same fallacy as trusting that a secret store is encrypted because its API returned success. The following is the minimum proof, and it applies to every candidate.

Start from a running daemon. These install lines come from each project's own documentation and were not executed on a Windows machine in any pass — verify them against current docs before pasting.

# psmux        — winget install psmux.psmux            (or: scoop install psmux)
# Zellij       — winget install zellij-org.zellij      (or the v0.44.3 msvc .msi)
# rmux         — winget install Helvesec.rmux          (or: scoop install rmux / choco install rmux)
# herdr        — download the dated preview asset: tag preview-2026-07-29-44b3adb12552,
#                asset herdr-windows-x86_64.zip   (NOT stable v0.7.5 — it has no Windows asset,
#                and the bare tag `preview-2026-07-29` does not resolve)
# wezterm-mux  — download WezTerm-windows-nightly.zip  (no current tagged release)
# OpenCode     — winget install anomalyco.opencode

Step 1 — start a session, then kill the parent terminal outright. Not "close the window", not Ctrl-C: kill it the way a crash kills it, because a graceful close lets a well-behaved child clean up and tells you nothing about detachment.

Read this before you run it, or you will kill the terminal you are typing in. (Get-CimInstance …).ParentProcessId from a pane inside Windows Terminal or ORCA returns the host process, and killing that host closes every tab in that window — including terminal B, from which you were about to run the kill. Launch terminal A as its own console host so the two are genuinely separate processes:

# Terminal B (the one you keep) — launch terminal A as a SEPARATE console host:
$tool   = 'psmux'
$parent = Start-Process pwsh -PassThru -ArgumentList '-NoExit','-Command',"$tool new-session -d -s probe"
# NOTE for Zellij: there is no verified detached-start invocation. See the Zellij entry —
# `zellij --session X options --help` only prints help and creates nothing.

# Confirm the daemon exists BEFORE you kill anything — it is the thing under test:
Get-Process | Where-Object { $_.ProcessName -like "*$tool*" } | Select-Object Id, ProcessName, StartTime

# Now kill terminal A the way a crash would. No /T, no politeness:
Stop-Process -Id $parent.Id -Force

Do not use taskkill /T, which walks ParentProcessId and will kill a correctly-detached daemon anyway — that measures the process tree, not the design.

The harsher variant is worth testing separately and knowingly, and worth describing accurately, because the usual description of it contains a claim this guide cannot support. A parent that puts its children in a Job Object with JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE tears everything down when the job handle closes — unless the child was created with CREATE_BREAKAWAY_FROM_JOB and the job permits it via JOB_OBJECT_LIMIT_BREAKAWAY_OK. So "regardless of how the child was created" is wrong: breakaway is exactly how a child escapes, and it is why rmux spawns with CREATE_BREAKAWAY_FROM_JOB and has an explicit refusal path rather than a silent degrade. (Note the constant's real name: JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE. The bare KILL_ON_JOB_CLOSE that circulates is a grep string, not an API symbol.) And the frequent addendum — that this Job-Object teardown "is why Windows OpenSSH cannot host persistent sessions" — is not established: it rests on one maintainer sentence, and code searches for CreateJobObject and KILL_ON_JOB_CLOSE in openssh-portable return zero hits. What is established is that Windows sshd has no tmux equivalent. Why is not.

Step 2 — open a fresh terminal, list, and reattach.

psmux ls          # or: zellij list-sessions / rmux ls / herdr list / wezterm cli list
psmux attach -t probe

If the session is still listed and re-attachable, it is L2 and it will survive the ADE. If it is listed but you cannot get back into it, the tool is a supervisor, not a session host. If it is gone, it is L1 — which is precisely what disqualified upterm, an otherwise excellent project with real ConPTY, Job Objects and the only public-key auth model in the census.

Step 3 — drive a verb with no console attached at all. This is the headless-first constraint stated as a test, and it is the step everyone skips. zellij action, tmux send-keys and wezterm cli are normally invoked from inside a terminal; whether they still function when a Scheduled Task, a Windows Service or a Claude Code hook invokes them with no console and no TTY on stdin/stdout is a different question, and nobody has run it for any candidate. It is disqualifying if it fails, because a middle layer you can only drive from a terminal is not a middle layer — it is a multiplexer. Two hours settles it for the whole field at once (Q23), which is why it is the cheapest disqualifier in this document.

Run this PowerShell elevated. New-ScheduledTaskPrincipal -LogonType S4U needs an administrative token to register, and the account needs the "Log on as a batch job" right. Getting either wrong produces an error at Register-ScheduledTask, which is at least loud.

# Run every candidate's list verb from a console-less caller, as the SAME principal that
# owns the daemons, capturing both streams to a file.  ELEVATED PowerShell required (S4U).

New-Item -ItemType Directory -Force C:\probe | Out-Null

# 1. Resolve ABSOLUTE paths first. A Scheduled Task does not reliably inherit the *user*
#    PATH, and winget/scoop put all four of these under %LOCALAPPDATA%. Without this the
#    task reports CommandNotFound for everything and you conclude the whole field fails a
#    column it was never given a chance at. This guide documents the same class of failure
#    for sandbox-runtime: per-user installs "resolve on the inherited PATH but cannot be
#    opened by the sandbox account".
$bins = 'zellij','psmux','wezterm','rmux' | ForEach-Object {
    $c = Get-Command $_ -ErrorAction SilentlyContinue
    if ($c) { $c.Source } else { Write-Warning "not on PATH here either: $_"; $null }
} | Where-Object { $_ }
$bins   # sanity-check these are real absolute paths before continuing

$verbs = @(
    "& '$(($bins | Where-Object { $_ -match 'zellij'  }))' list-sessions"
    "& '$(($bins | Where-Object { $_ -match 'psmux'   }))' ls"
    "& '$(($bins | Where-Object { $_ -match 'wezterm' }))' cli list"
    "& '$(($bins | Where-Object { $_ -match 'rmux'    }))' ls"
) -join '; '
$cmd = "& { $verbs } *> C:\probe\out.txt"

# 2. Register as the same principal, domain-qualified so it is unambiguous on a
#    domain-joined machine.
$act = New-ScheduledTaskAction -Execute 'powershell.exe' `
       -Argument "-NoProfile -NonInteractive -Command `"$cmd`""
$pri = New-ScheduledTaskPrincipal -UserId "$env:USERDOMAIN\$env:USERNAME" -LogonType S4U
Register-ScheduledTask -TaskName ProbeNoConsole -Action $act -Principal $pri -Force
Start-ScheduledTask  -TaskName ProbeNoConsole

# 3. WAIT for it to finish. A fixed Start-Sleep races the task, and a partial or empty
#    file reads as a failure under this test's own pass rule.
$deadline = (Get-Date).AddSeconds(60)
while ((Get-ScheduledTask -TaskName ProbeNoConsole).State -eq 'Running' -and (Get-Date) -lt $deadline) {
    Start-Sleep -Milliseconds 500
}
"final state: $((Get-ScheduledTask -TaskName ProbeNoConsole).State)"

Get-Content C:\probe\out.txt
(Get-ScheduledTaskInfo -TaskName ProbeNoConsole).LastTaskResult   # 0 does NOT mean it worked
Unregister-ScheduledTask -TaskName ProbeNoConsole -Confirm:$false

Four rules for reading the result, and three of them are ways to avoid a false disqualification.

  • An empty output file with a zero exit code is a failure, not a pass. Read the file, never the return code: it must contain the session list you created from an interactive shell in step 1.
  • A CommandNotFoundException in out.txt is a PATH failure, not a console failure. That is the single most likely wrong answer this test produces, and step 1 of the block exists to prevent it. If you see one, fix the path and re-run; do not record a disqualification.
  • Run it as the same principal and at the same integrity level. Same user is not sufficient: rmux computes its pipe name from SID and integrity level, so an elevated task probing daemons started from a non-elevated shell — or the reverse — is a guaranteed false negative for rmux specifically, and it will look exactly like a console-independence failure. Start the daemons and run the probe at the same elevation, or test rmux separately.
  • Be precise about what this proves. A console-subsystem process launched by Task Scheduler is normally still given a console object unless it is created with CREATE_NO_WINDOW. What this test definitely establishes is no TTY, no interactive window, non-interactive session — which is the realistic shape of a hook or a service caller and is what the headless-first constraint is actually about. If you want the unambiguous version, wrap the verbs in a script that calls FreeConsole() first — the P/Invoke for it is in the attach probe above — and compare the two results.

Those three steps are the minimum. Two more are worth the time if the candidate survives them:

Step 4 — nest it, and time the spawn. Run the candidate as an agentCmdOverrides target inside an ORCA pane — the shape of that override entry, and the three things to get right in it, are in If you are wrapping under ORCA specifically — then resize the pane, run an alt-screen app and a mouse-mode app inside it (Q18). Then time the spawn, which is one line:

Measure-Command { & psmux new-session -d -s timing ; & psmux kill-session -t timing }

If that reports seconds rather than milliseconds, you have found the startup handshake problem — ConPTY's DA1 query and its 3-second wait, which one measured data point took from 2121 ms to 142 ms by answering instantly. The quotes, the attribution (a third-party developer, not Microsoft) and the state of the thread they live in are under The four things a middle layer must get right on Windows. A related failure is worse than slow: with PSEUDOCONSOLE_INHERIT_CURSOR set, the child can wedge inside process initialisation waiting for a CPR reply that never comes — hung indefinitely, with nothing in its logs and nothing to attach a debugger to.

Step 5 — prove the resize path. Resize the ORCA pane and check the size the session actually reports, from inside it. That is the same loop the mechanism section uses, run in the nested position rather than the outer one:

# Run this INSIDE the nested session, then drag the ORCA pane border.
while ($true) { '{0}x{1}' -f [Console]::WindowWidth, [Console]::WindowHeight; Start-Sleep 1 }

There is no SIGWINCH on Windows; ResizePseudoConsole(HPCON, COORD) is an explicit call by whoever owns the handle, at every hop, so this loop is testing whether the call is being made on the inner hop at all. The specific thing to watch for is psmux's in-band sizing: it asks the outer terminal CSI 18 t, waits 500 ms, and falls back to 120x30 if nothing answers — and whether Electron/xterm.js answers by default is unknown, because xterm.js gates windowOptions behind opt-in flags. Numbers frozen at 120x30 are that fallback; numbers frozen at anything else are a missing ResizePseudoConsole call. Either way every TUI inside then wraps against a size nothing on screen agrees with, which is a silent-corruption-class bug rather than a cosmetic one. It is Q4, it costs an hour, and it is the fastest way to turn an unknown into a fact.


Open questions

Twenty-six questions, each with a concrete next step, a cost, and what the answer changes. The first tier can each eliminate whole branches; the last tier is due diligence you pass before shipping, not a discriminator between candidates.

On the order. The table below is grouped by what an answer eliminates, which is not the same as the order to run them in. The order to run them in is in The running order, with wall-clock above, and it differs in one way worth stating here: Q23 goes first, then Q1a, and Q9 starts in parallel on day one. Q9 has the highest leverage of anything in this document and takes a week; Q23 takes two hours and can kill the entire PTY branch; Q1a takes two hours and can moot Q1 entirely. Two hours that eliminate eleven candidates beat a week that eliminates eleven candidates, and because Q9 is passive logging you do not have to choose.

Tier 1 — answers that eliminate branches.

# Question Next step Cost What the answer changes
1a Can ORCA's agentCmdOverrides produce a raw-pipe child at all? ORCA spawns through node-pty, i.e. always a ConPTY, and the documented -CC remedy is "give the child raw pipes". Ask whether the cure exists before measuring the disease Read ORCA's spawn path from tui-agent-launch-command.ts through to the node-pty call; check whether any override shape bypasses the PTY 2 hours Yes → Q1 is worth running and -CC stays in play. No → -CC control mode is unreachable in this topology whatever Q1 finds; psmux drops to its plain CLI verbs and the "strongest interface" claim does not apply to you
1 Does ORCA's node-pty ConPTY actually corrupt -CC control mode, as psmux's source warns for the SSH case? Run psmux -CC as an agentCmdOverrides target in ORCA and diff the %output stream against the same command run from a raw-pipe parent One afternoon Corrupted → apply 1a's remedy or drop -CC for CLI verbs plus polling. Clean → the SSH-scoping in the source was literal, the hazard does not generalise, and psmux's control mode is the best interface available to you
9 Is the no-PTY architecture sufficient for your actual sessions? claude -p --input-format stream-json plus HTTP hooks plus OTel covers everything except live screen fidelity and a human typing mid-run. This is the highest-leverage item here Instrument one week of real work: count sessions where you actually attached and typed mid-run against sessions you only observed One week, passive Sufficient → stop evaluating multiplexers; build on pipes and hooks, and the PTY-ownership problem disappears entirely. Not sufficient → you have a measured number for how often you need attach, which is the input to every remaining trade-off
3 Does Zellij's use_vt_path() gate hurt inside an ADE PTY? It selects the native-console path when neither TERM nor WT_SESSION is set. There is currently no pass/fail criterion — nobody knows what the failure would even look like Launch Zellij under an ORCA pane, record the observable behaviour (keyboard responsive? input garbled? fine?), then re-test with TERM=xterm-256color forced 1 hour Broken → one env var in the override command fixes it, and Zellij stays the most mature candidate. Fine → the most-cited hazard in three editions of this research was never real
23 Do any candidate's verbs work with no console attached? Every headless-verbs claim is untested against a genuinely console-less caller — a service, a Scheduled Task, or a hook. This is the headless-first constraint stated as a test Invoke zellij list-sessions, psmux ls, wezterm cli list and rmux ls from a Scheduled Task running with no console, capturing stdout/stderr to a file 2 hours Work → the whole headless-verbs column becomes trustworthy. Fail → those candidates are terminal tools, not middle layers, and the no-PTY architecture wins by default

Tier 2 — answers that pick between surviving candidates.

# Question Next step Cost What the answer changes
2 What does herdr's "Direct terminal attach | unsupported" mean, given the same page lists "Local persistent sessions" and "Windows Terminal / PowerShell app attach" as Supported (beta)? Demoted from top priority because the two tables together suggest it is the Unix fd-passing path, not the ADE-spawn path Read src/api/, src/ipc.rs and the Windows-beta doc's surrounding sections; if ambiguous, file one question on the repo 2 hours Unix-only mechanism → herdr is a live candidate with a 23k-star maintenance base. The ADE path → herdr is out on Windows until the beta lands
8 Can wezterm-mux-server be driven headlessly end to end on Windows — spawn, send-text, get-text, list, with a client attaching later? Install the nightly Windows zip, run wezterm-mux-server --daemonize, drive it with wezterm cli only Half a day Yes → you get the only version-skew-tolerant codec available, at the price of tracking a nightly. No (attach needs the GUI) → the codec remains a design reference only
4 Does Electron/xterm.js answer XTWINOPS CSI 18 t? Read xterm.js windowOptions defaults; then empirically resize an ORCA pane and check the reported size inside psmux 1 hour Answers → psmux's in-band sizing works and resize is solved. Does not → psmux silently runs at 120x30 forever, which is a silent-corruption-class bug, not a cosmetic one
10 Is ao pty-host extractable as a standalone package? Apache-2.0 makes vendoring clean and it is the closest architectural match found Try go build ./backend/internal/adapters/runtime/conpty/... outside the AO tree and see what it drags in 3 hours Clean → you have a working detached-ConPTY host to build on rather than write. Entangled → it is a design reference and you write your own
12 Do OpenCode's /pty endpoints work on native Windows with the shipped binary? opencode serve, then POST /pty plus a WS connect from PowerShell 1 hour Work → a REST+WS attach surface with a resumable cursor exists today — but note it is L1+, not L2, and the wire is UTF-8 text. Fail → the protocol is still worth copying
18 Do any candidates run correctly nested inside a second ConPTY? Nobody has tested this, including oly — whose "nested agent tests" turned out not to exist Run each under an ORCA pane with a resize, an alt-screen app, and a mouse-mode app One day for all four Whichever survives is your candidate. This is the single largest untested area
24 Can any candidate's prefix key be disabled entirely on a headless path, not merely rebound? Only docker documents remapping; every multiplexer's rebindability is an inference For each of psmux, Zellij, rmux: find the config key, set it to none, and verify no chord is intercepted 3 hours Yes → prefix collision with the ADE stops being a concern. No → you inherit a keybinding negotiation with every ADE you ever swap to, which is exactly what this project exists to avoid

Tier 3 — soundness and due diligence before adoption.

# Question Next step Cost What the answer changes
6 Is psmux's cross_session_server.rs path reachable in practice? It accepts the first connection with no handshake, registers it as a tee writer so it receives all pane output, and writes everything it reads into the ConPTY — full-duplex exfiltration plus injection on a loopback port Trace every call site of cross_session_server, or attempt a proof-of-concept connect during a cross-session pane move 3 hours Reachable → psmux is unsafe on a shared or multi-user machine until patched; report upstream. Unreachable → it is latent, and still a reason to pin and watch
7 Does quil's Windows ConPTY stack actually work? 25+ Windows source files, ubuntu-only CI, zero external issues ever filed Run its test suite on Windows; add a windows-latest job locally and see what breaks Half a day Works → you get the richest agent-facing surface available (18 MCP tools). Breaks → the MCP surface is a design reference only
13 Does oly's named-pipe ACL gap still exist, or was it closed without updating the roadmap doc (untouched since 2026-03-12)? Diff src/ipc.rs across the full commit history for any ACL or security-descriptor change; or ask the maintainer 1 hour Closed → oly's main disqualifier is gone. Still open → oly is unusable on any machine with another local account or a low-integrity process
22 Do the four small candidates have CVE/GHSA records? Only their own issue trackers were checked One NVD/GHSA sweep before adoption sign-off 1 hour Either way this is a gate you pass before shipping, not a discriminator
5 Is AttachConsole reliable in the exact topology that matters — a process spawned inside a headless ConPTY created by node-pty inside Electron? Write a 30-line helper that attaches to a claude.exe spawned by ORCA and calls GetConsoleProcessList 2 hours Works → you gain a read-only out-of-band inspection path. Fails → the late-attach refutation is complete and that door is closed permanently
11 Does codex app-server --listen unix:// actually bind on Windows? The uds crate is cross-platform and the transport is registered unconditionally, but the daemon lifecycle manager is explicitly Unix-only codex app-server --listen unix://%TEMP%\test.sock on Windows 11 and see 30 minutes Binds → a second vendor-native no-PTY architecture is available. Fails → Codex stays stdio-only on Windows
14 Is oly's main-branch stall (no commits since 2026-07-01, though four side branches are active) a stable plateau or a departed maintainer? Watch for 4-6 weeks; check issue response latency Passive Affects only whether oly is worth adopting versus reading
19 Should the middle layer's control plane carry ACP over a Windows named pipe? The spec formally permits it and agent CLIs are converging on it — but ACP has no Windows CI and no live-process attach primitive Prototype the transport shim against the reference implementation and see whether it builds and passes on Windows Two days Builds → the wrapper becomes protocol-native to the ecosystem it serves. Fails → use your own framing and revisit when ACP v2's terminal work lands
17 Has microsoft/terminal's in-process ConPTY redesign shipped, partially shipped, or stalled? Cross-check against Windows Terminal changelogs and the ConPTY NuGet release notes 1 hour Shipped → the reflow/resize desync may be fixable rather than permanent. Stalled → plan around it
15 Would kind or a Hyper-V-backend Podman work end to end on Windows without WSL? Architecturally plausible from both projects' docs; zero real-world confirmations located One hands-on spike: podman machine init --provider hyperv then kind create cluster Half a day Only matters if you want container isolation around the middle layer; it never replaces it
21 Does any libghostty consumer have a working Windows build today? ghostel says yes for its own ConPTY module — is that libghostty on Windows, or ghostel routing around it? Read src/ConPtyProcess.zig in dakra/ghostel and check what it imports 1 hour libghostty → a fourth VT-parser option opens up on Windows. Routing around → avt and vt100-psmux remain the only two
25 Can Eclipse Theia's backend be packaged and run headlessly on Windows? Its base-terminal-server.ts:49 async attach(id: number) against a backend terminal registry is a second independent implementation of VS Code's ptyHost primitive, in a 21.6k-star EPL-2.0 project, and its shell process explicitly handles Windows (shell-process.ts:90-91) Try to build and start Theia in server mode on native Windows and drive the terminal REST/WS surface with no browser attached Half a day Works → a second, better-licensed prior-art implementation becomes an actual option rather than a reference. Fails → it joins VS Code's ptyHost as prior art you read rather than run
20 Is there anything transferable in SCADA/industrial session tooling? Genuinely under-researched, not confirmed absent One dedicated search round before writing the area off 2 hours Low expected value; recorded so the gap is not mistaken for a finding
16 Does the Windows Hypervisor Platform feature work on Windows 11 Home? No Microsoft doc states an edition requirement either way Only resolvable empirically or by a Microsoft support statement Low priority — the tool that needs it (sbx) is closed-source and already out on the open-source constraint

Coverage gaps

Stated plainly so nothing here is mistaken for verified, and ordered by how much damage a wrong assumption does. Inferences lead, because they are the items most likely to be read as facts. Declared unknowns beat silent ones.

1. Inference presented as reasoning, never observed. Each of these is a conclusion reached from verified premises. Each could be wrong without any of its premises being wrong.

  • That a foreign-SID console is not AttachConsole-able (the sandbox-runtime incompatibility) — inferred from documented per-session console semantics, never tested.
  • That the session-0 boundary blocks AttachConsole — same class, same status. It is commonly stated as established fact. It is not.
  • That the psmux -CC / ConPTY DCS hazard generalises beyond SSH. The source scopes it to "when running over SSH with a ConPTY console"; the generalisation to a node-pty ConPTY is this document's, and Q1 exists to test it.
  • That claude -p --output-format stream-json behaves identically on Windows and Linux — inferred from unqualified flag documentation over ordinary pipes. This is the no-PTY route's load-bearing cross-platform claim and nobody has run it on both platforms.
  • That asciinema/avt compiles on native Windows — inferred from a two-crate dependency list (rgb and unicode-width) with no platform-conditional code. Nobody has built it.
  • That eat inherits Emacs core's Windows-pty gap.
  • That Windows Job Objects are what kill sshd's children on disconnect — asserted in one maintainer sentence, with zero code-search corroboration in openssh-portable.
  • That the multiplexer candidates' prefix keys are rebindable — inferred from "they have config files". Only docker's --detach-keys is documented.

2. Not tested empirically anywhere. This is a source-and-docs map. Almost nothing in it has been run.

  • Double-ConPTY nesting, by any candidate — including oly, whose supposed nested-agent tests were found not to exist.
  • Any candidate actually spawned by ORCA.
  • Whether AttachConsole reaches an ORCA-spawned agent.
  • Whether any candidate's CLI verbs work with no console attached — the headless-first constraint has never been tested against a genuinely console-less caller.
  • What the observable symptom would be if Zellij takes the native-console input path inside an ADE PTY — so Q3 currently has no pass/fail criterion.
  • The install commands in this guide were assembled from each project's own docs; none was executed on a Windows machine.
  • The PowerShell probes in How to check any of this yourself are new code written for this guide. The Win32 semantics behind them are cited; the scripts themselves have never been run. The same is true of the System.Net.WebSockets.ClientWebSocket block for OpenCode's /pty, which is additionally written against an undocumented API whose request bodies and parameter names are guesses from the route handlers.
  • Zellij has no verified detached-start invocation anywhere in this guide. zellij --session X options --help prints help and creates nothing, and no -d equivalent was located. This blocks step 1 of the verification ritual for the most mature candidate in the census.

3. Not re-verified in any pass. Believed, sourced once or not at all, never re-checked. Pruned to the items this guide actually leans on — a caveat you cannot attach to a sentence is noise.

  • Zed's agent_servers ACP-shaped override — one of the four native-Windows ADE override hooks named in the opening section, and the only one of the four never re-verified.
  • The closed-source ADE entries were never verified at source, by definition. Note what this does not cover: Warp's crates/local_control is in the AGPL-3.0 open-source client and was read at source; it is Warp's commercial product and its Oz cloud API that are unverified.
  • rmux's "936 of ~1000 commits" single-author statistic — recorded once, never re-verified, and it is the load-bearing number behind the bus-factor argument in the Linux-parity tiebreak.
  • The rmux breakaway error string's file and line ("breakaway process creation denied…") — the string is quoted from a source read once; the behaviour is verified, the pointer is not.
  • PowerSession-rs's missing-ResizePseudoConsole finding — verified at source, but the file and line were not recorded and could not be recovered.
  • oly's extract_query_responses_no_clientresolved during this revision, and the claim it supported was wrong. The symbol is in src/session/pty.rs; the function answers CPR, DSR and OSC 10/11 in detached mode and explicitly refuses DA1, which is the query behind the startup stall. "oly answers ConPTY's startup handshake" is corrected wherever it appeared.
  • cmux's L3 FAQ quote and ripple's HANDOFF_GARBLING.md — both read once with no URL recorded. The ripple document is additionally in Japanese, so any English sentence attributed to it in this guide or elsewhere is a translation rather than a quotation.
  • The build >= 22621 passthrough threshold — corroborated by psmux and rmux converging on the same constant, but neither constant's file path was recorded, so the corroboration is not locatable from here.
  • The pywinpty maintainer's Jupyter Discourse post (>30M downloads, "maintained by a single person") — quoted in the terminado entry, URL never recorded, never re-retrieved.
  • The LangChain agent-inbox 'response' discriminator — an API detail from one pass with no source, kept only because that route is in the non-multiplexer table.
  • Several citations record a method (gh api, "README") rather than a retrievable object, and a handful resolve only to a repository rather than a line. The instances that matter are the ones enumerated above; the rest support claims nothing in this guide depends on.

4. Never examined at all.

  • The xterm.js attach-addon raw-bytes-over-WebSocket convention, shared by ttyd, code-server and VS Code. This is the largest single hole in the protocol coverage — and the superlative usually attached to it ("arguably the most widely deployed…") is itself unverified.
  • chadbyte/claude-relay and myrialabs/ptykit ("PTY sessions over WebSocket — collaborative rooms, resilient client, Node & Bun", 1 star, pushed 2026-07-30).
  • ACP v2's terminal work; Codex's stdio-to-uds crate and its third remote_control transport.
  • Whether any candidate other than herdr documents Windows prefix-key behaviour.

5. Evidence that could not be retrieved. Both blocked cases are the same mechanism — an Anubis-style proof-of-work interstitial that returns HTTP 200 with a "Making sure you're not a bot!" body, so a naive status check passes it.

  • developer.valvesoftware.com — the Source RCON spec was not independently verifiable.
  • gitlab.freedesktop.org — the canonical OSC 133 spec; triangulated from three implementer docs instead.
  • The abduco/dvtm COSIN'18 slides did not extract.

6. Coverage bounds worth stating. The candidate pool came from GitHub topic and code search plus targeted follow-up. Anything not indexed by GitHub code search — self-hosted forges, unreleased internal tools, and non-English project descriptions beyond the handful encountered — is outside this survey's reach by construction, not absent from the world. Star counts and last-activity dates are a 2026-08-02 snapshot and churn weekly.


Primary sources by category

Microsoft ConPTY and console documentation. CreatePseudoConsole · ResizePseudoConsole · AttachConsole · WriteConsoleInput — the "no longer part of our ecosystem roadmap" banner and the separate wrong-way-verb tip · ReadConsoleOutput · Classic console vs virtual terminal — the "cross security and privilege-levels" framing · conpty-static.h — the current public ConPTY flag list · winconpty.cpp — the only three dwFlags consumers in the implementation · microsoft/terminal doc/specs — home of #4999 improved keyboard handling and #13000 in-process ConPTY · CreateNamedPipe — a NULL security descriptor grants Everyone read access · tscon — "You can't connect to the console session"

Candidate source repositories. psmux · Zellij · rmux · herdr · oly / open-relay · qscreen · quil · WezTerm — wezterm-mux-server and the varint codec (note the repository moved: wez/wezterm 301-redirects here; GitHub's licence API reports NOASSERTION and the file is LICENSE.md) · OpenCode — /pty and opencode serve · Agent Orchestrator — ao pty-host · upterm · microsoft/node-pty — what ORCA uses · rprichard/winpty — the pre-ConPTY scraper, superseded · NVDA — production late-attach reader · cc-discord-remote — the only working late-attach loop found · anthropic-experimental/sandbox-runtime · PowerSession-rs · dakra/ghostel — ConPTY under a swappable frontend, src/ConPtyProcess.zig (the repository is ghostel, not ghostel.el; the latter 404s) · asciinema/avt — the VT parser with no platform code · Podman (containers/podman redirects here) · Eclipse Theia — a second attach-by-id implementation · ttyd · sshx · code-server · mobydeck/atch — the worked example · Orc/screen — the preserved GNU screen beta-test README · dtach · x3270 — headless scriptable emulator with -httpd · warpdotdev/warp — crates/local_control in the AGPL-3.0 client

ADEs with a documented free-form command override (the hook a middle layer needs). ORCA — agentCmdOverrides · kandev · Pane · Zed — agent_servers, ACP-shaped, never re-verified · cmux — macOS only, and the census's only L3 claim

Field reports and third-party writeups (weaker tier than a project's own documentation, and labelled as such wherever cited). Laurent Kempe — multi-week psmux plus Copilot CLI on native Windows, the only external empirical corroboration in this research · Galen Guan — when a multiplexer is essential infrastructure and when it is an unnecessary layer · abduco/dvtm — "window and session management shouldn't be intermingled" · OpenAI — "maintaining MCP semantics in a way that made sense for VS Code proved difficult"

Protocol and interface specifications. tmux(1) — CONTROL MODE · WezTerm mux codec — varint framing built for version-skewed client/server pairs · WezTerm multiplexing — "Unix domains are supported on all systems, even Windows" · Agent Client Protocol — transport-agnostic by spec, so ACP over a named pipe is conformant (zed-industries/agent-client-protocol redirects here) · Codex app-server README — websocket labelled experimental/unsupported · MCP SEPs — zero terminal SEPs among 41 Final · asciinema ALiS streaming protocol — relay holds full VT state for late joiners · nREPL — the narrow-waist design precedent that predates LSP (the bare nrepl.org redirects here; the quoted passage is on this page, not the site root) · Evennia documentation — the Portal/Server passage is on the Portal-And-Server page, not the docs landing page, whose URL was never recorded here · Erlang distribution security — cookie auth is "not cryptographically secure" · Claude Code hooks — five hook types, the SessionStart/Setup exclusion, terminalSequence · Claude Code CLI reference — --input-format stream-json and companions · Claude Code monitoring — the 34 claude_code.* identifiers · OpenCode server — "the TUI is the client that talks to the server" · docker attach --detach-keys — the only documented prefix remapping in the census · Podman for Windows — the Hyper-V provider · minikube drivers — Hyper-V listed preferred (so is Docker; both carry the label) · Docker Desktop Kubernetes — the bundled cluster is kind or kubeadm · oh-my-posh shell_integration — OSC 133 in cmd.exe via Clink v1.14.25+

Issue-tracker threads, with their state disclosed. microsoft/terminal#7019 — DA1 3-second wait, "answer them", the ConPTY NuGet refusal-to-backport; closed not_planned 2023-09-29, maintainer comments continue after closure · microsoft/terminal PR #17510 — "Goodbye VtEngine Edition", VT output now passed through unmodified; merged 2024-08-01 · microsoft/terminal#1173 — closed completed two seconds after that merge · microsoft/terminal#19621 — passthrough breaks tmux control mode; filed by a Microsoft member, open · microsoft/terminal#15976 — reflow/resize desync megathread, "this is a Hard problem"; open · microsoft/terminal#17384 — >5 KiB paste deadlocks the terminal; open since 2024-06-06 · microsoft/terminal#17643 — residual VT-passthrough issues; open (cooked-read reflow, ScrollConsoleScreenBuffer, DA3 truncation and the PSReadLine SGR issue are fixed) · microsoft/terminal#5468 — "no reason to ever use ReadConsoleOutput"; closed Resolution-By-Design seven minutes after filing · PowerShell/Win32-OpenSSH#2291 — no tmux equivalent on Windows sshd; filed by a Microsoft member, open · pywinauto#492 — the 2022 empirical late-attach attempt; open since 2018 · node-pty PR #886 — AttachConsole failed fix; open, unmerged · node-pty PR #901 — Node 22 resize-after-exit; open, unmerged · node-pty#921 — ptyHandles data race; fixed 2026-05-13 · node-pty PR #943 — conout worker deadlock, a regression of an already-fixed bug; merged 2026-07-31 · microsoft/vscode#201029 — the kill() root cause, still open · openai/codex#25415 — the same crash in a shipping product; open · stablyai/orca#9586 — ORCA's own instance; closed with zero comments, the diagnosis is the reporter's own · openai/codex#11750 — the fragility of file-based IPC; open. Two separate sentences are usually spliced into one quotation here — see the non-multiplexer routes section · tsl0922/ttyd#1501 — the shipped Windows binary cannot spawn a child on build 26200; closed 2026-03-19 · anthropics/claude-code#24365 — claude serve request; closed not_planned, stale-bot 2026-03-14, locked 2026-03-21 · anthropics/claude-code#6686 — ACP-over-network request, 551 reactions; closed not_planned 2026-02-09 · awslabs/cli-agent-orchestrator#182 — the in-place TUI redraw that makes screen-scraping go stale; closed as completed 2026-04-20, so it proves the failure mode is real, not that any tool is broken today · zellij-org/zellij#4745 — the Windows rough-edges tracking issue; open, opened by imsnif (project lead), with divens (contributor) as its heaviest commenter · dagger/container-use PR #252 — "#242 added Windows compilation but no installation method"; open since 2025-07-24 · loft-sh/devpod#1946 — open, and the quoted sentences are the issue body, written by a third party (skevetter, no repository association) proposing his own fork, with his own hedge "it is fairly safe to say". Not a maintainer statement — the unhedged evidence of abandonment is the release date · tmux#5237 — why a nested layer cannot forward OSC 133; open, both quotes from maintainer nicm but from two separate comments · ENiGMA½#175 — "socket descriptors cannot be shared" in Node.js, solved with a Rust sidecar; closed since 2018-12-09 · GNU Emacs bug#71472 — the ConPTY patch de-tagged 2025-02-12, dormant at wishlist severity

Sources consulted — provenance manifest

Machine-generated record of every retrieval performed while researching the accompanying report. 395 retrievals across 3 tools.

Each row is one retrieval: what was asked, whether it returned content, and when. This is the audit trail — it lets you re-run any query or re-fetch any page and check the report against what the sources actually say.

The retrieved payloads themselves (about 8 MB of third-party page text) are deliberately not reproduced here; the URLs below are the canonical copies.

duckduckgo-search — 149 retrievals (149 returned content)

# Query or URL Status Results When (UTC)
1 ORCA agentic development environment github ADE ok 36 20260729_202648_675
2 tmux native windows port impossible pty ok 32 20260729_202651_628
3 AttachConsole WriteConsoleInput another process console Windows ok 42 20260729_205158_452
4 ConPTY pseudoconsole handle DuplicateHandle another process attach ok 34 20260729_205201_700
5 AttachConsole fails Windows Terminal ConPTY headless conhost session ok 33 20260729_205242_360
6 MCP server terminal control persistent session Windows ConPTY ok 50 20260729_205428_324
7 terminado pywinpty jupyter terminal windows ok 37 20260729_205431_028
8 abduco dtach windows port reimplementation ok 25 20260729_205447_020
9 Windows 分离 会话 终端复用 tmux 替代 ok 31 20260729_205454_031
10 vscode extension API createTerminal onDidWriteData headless automate ok 21 20260729_205508_990
11 terminado pywinpty windows support github issue ok 28 20260729_205511_825
12 atch terminal session tool github persistent logs detach reattach ok 31 20260729_205525_912
13 reptyr how it works ptrace steal tty README nelhage ok 28 20260729_205531_835
14 AttachConsole different user session elevated process fails ERROR_ACCESS_DENIED ok 28 20260729_205537_541
15 Conductor app coding agents Windows support ok 16 20260729_205848_839
16 Warp terminal agent API automate driving sessions documentation ok 16 20260729_205907_722
17 Warp terminal Windows download official ok 12 20260729_205939_045
18 MSYS2 package tmux pacman install ok 43 20260802_113045_182
19 cygwin pty emulation named pipes winpty native console programs not tty ok 43 20260802_113048_184
20 tmux control mode -CC protocol specification iTerm2 ok 43 20260802_115310_449
21 Agent Client Protocol Zed agentclientprotocol.com spec terminal ok 35 20260802_115408_045
22 Windows OpenSSH Server ConPTY session survives disconnect nohup SIGHUP equivalent ok 34 20260802_115422_722
23 Windows Session 0 isolation service cannot interact with desktop Microsoft Learn ok 42 20260802_115428_409
24 modelcontextprotocol MCP terminal tool convention shell command standard schema ok 47 20260802_115505_959
25 Jupyter terminal REST API WebSocket terminado protocol xterm.js ok 25 20260802_115512_775
26 OpenCode HTTP server API session control documentation sst opencode ok 27 20260802_115520_614
27 Debug Adapter Protocol DAP design rationale Microsoft json-rpc origin story ok 32 20260802_115524_391
28 RDP TeamViewer AnyDesk remote support tool Windows console session architecture how it works ok 31 20260802_115532_078
29 CreatePseudoConsole ConPTY Windows service session 0 no interactive desktop headless ok 21 20260802_115534_293
30 Microsoft legacy console API ReadConsoleOutput WriteConsoleOutput deprecated discouraged Windows Terminal virtual terminal sequences ok 15 20260802_115542_724
31 Windows nested job objects Windows 8 breakaway JOB_OBJECT_LIMIT_SILENT_BREAKAWAY_OK ok 21 20260802_115546_055
32 pywinauto win32console AttachConsole automate console output python library ok 31 20260802_115552_138
33 GetConsoleProcessList unreliable fails race condition console attach helper ok 15 20260802_115604_327
34 asciinema asciicast v2 format specification github ok 33 20260802_115614_050
35 node-pty github issue Windows service session 0 conhost fails no console ok 34 20260802_115614_561
36 Windows containers process isolation vs Hyper-V isolation Linux container support ok 19 20260802_115626_577
37 PowerSession-rs Watfaq windows terminal session recorder ok 23 20260802_115627_265
38 GitHub Actions self-hosted runner Windows service architecture Runner.Listener Runner.Worker ok 30 20260802_115628_249
39 OSC 133 semantic prompt shell integration specification ok 29 20260802_115630_808
40 jupyter_server_terminals REST API endpoints api.yaml create terminal websocket message protocol stdin stdout ok 25 20260802_115632_298
41 VS Code terminal shell integration OSC 633 source code ok 29 20260802_115639_026
42 Docker Desktop Windows Hyper-V backend vs WSL2 backend requirement ok 29 20260802_115642_111
43 Claude Code session jsonl transcript file format tail ok 26 20260802_115643_344
44 devcontainer.json Windows without WSL Windows containers support ok 27 20260802_115651_625
45 opencode sst session storage format json ok 23 20260802_115655_142
46 k3s kind minikube Rancher Desktop Podman Desktop Windows backend requirements ok 31 20260802_115656_494
47 dagger container-use github coding agent isolation ok 27 20260802_115703_801
48 docker attach vs docker exec semantics interactive TTY long-running ok 28 20260802_115707_276
49 tmux pipe-pane log streaming man page ok 27 20260802_115709_790
50 kubectl attach vs kubectl exec semantics TTY reconnect ok 30 20260802_115711_077
51 termtosvg python pty terminal recording svg ok 30 20260802_115713_682
52 Anthropic sandbox runtime coding agent container isolation ok 31 20260802_115714_875
53 OpenChamber ADE agent development environment github ok 29 20260802_115718_716
54 Coder workspaces Daytona devpod Windows support requirements ok 27 20260802_115718_784
55 Buildkite agent architecture long polling no inbound port official docs how it works ok 28 20260802_115724_785
56 GitKraken Kepler coding agent Windows ok 22 20260802_115735_931
57 JetBrains Air Junie coding agent IDE ok 27 20260802_115740_153
58 Conductor app coding agents worktree GitHub ok 25 20260802_115743_166
59 Terragon labs coding agent orchestrator ok 26 20260802_115750_950
60 Factory.ai droid CLI coding agent Windows ok 25 20260802_115757_161
61 Imbue Sculptor coding agent github worktree ok 25 20260802_115759_983
62 Tembo AI coding agent development environment ok 31 20260802_115806_486
63 Windows Task Scheduler run whether user logged on or not session headless persistent ok 27 20260802_115806_997
64 Async coding agent orchestration app worktree ok 28 20260802_115809_191
65 uzi coding agent CLI parallel worktrees github ok 25 20260802_115813_057
66 OpenAI Codex CLI session rollout jsonl file resume headless non-interactive exec mode ok 19 20260802_115837_267
67 Ghostty libghostty design document Mitchell Hashimoto PTY escape sequence own the child process ok 23 20260802_115837_349
68 tmux nested session prefix collision send-prefix ok 27 20260802_115840_164
69 dtach man page does not interpret escape sequences transparent ok 31 20260802_115844_266
70 Ghostty terminal Windows support status native ok 26 20260802_115849_854
71 uzi devflowinc github parallel coding agents worktree ok 26 20260802_115907_970
72 psmux windows tmux reddit review complaints ok 20 20260802_115911_765
73 Teleport gravitational SSH service windows node native support ok 27 20260802_115915_463
74 Codebuff github open source CLI agent ok 23 20260802_115919_415
75 Apache Guacamole architecture guacd protocol proxy RDP VNC SSH how it works ok 19 20260802_115919_628
76 Sourcegraph Amp coding agent CLI Windows open source ok 28 20260802_115922_708
77 ConPTY minimum Windows 10 build 17763 pseudo console requirement ok 20 20260802_115934_716
78 zellij nested session detected warning ZELLIJ env var ok 32 20260802_115939_208
79 OpenHands All-Hands-AI runtime architecture event stream action observation docker sandbox ok 25 20260802_115940_506
80 minikube hyperv driver windows native no docker desktop no wsl ok 20 20260802_115940_606
81 modelcontextprotocol SEP terminal tool proposal discussion standardize execute_command ok 28 20260802_120000_543
82 Toad terminal agentic coding ACP universal interface github ok 31 20260802_120003_926
83 ConPTY ResizePseudoConsole no SIGWINCH equivalent Windows console resize signal ok 24 20260802_120010_358
84 PowerShell Start-Transcript Windows native session recording ok 31 20260802_120029_570
85 asciinema live streaming asciinema server rec cast ok 28 20260802_120038_280
86 openai codex app-server JSON-RPC thread/start thread/resume protocol documentation ok 30 20260802_120043_340
87 Podman machine Windows WSL2 Hyper-V provider requirements ok 26 20260802_120058_595
88 Docker Desktop Sandboxes feature microVM requirements WSL2 docs ok 20 20260802_120111_599
89 container-use dagger windows support issue github ok 25 20260802_120115_158
90 OpenHands Windows without WSL guide docs.all-hands.dev ok 28 20260802_120146_427
91 run console application Windows service noninteractive window station Service-0x0 works fine no desktop needed ok 29 20260802_120256_999
92 Sculptor Imbue container isolation agent windows support ok 20 20260802_120303_620
93 devpod.sh docker provider windows requirements WSL kubernetes provider ok 14 20260802_120321_713
94 docker json-file log driver ring buffer max-size rotation ok 34 20260802_120324_336
95 systemd journald ring buffer volatile ephemeral log design ok 20 20260802_120341_352
96 node-pty onData tee stream multiple consumers ok 25 20260802_120344_371
97 Windows Hypervisor Platform feature Windows Home edition supported requirements ok 10 20260802_120453_373
98 docker attach ctrl-c killed container accidentally SIGKILL github issue ok 31 20260802_120501_652
99 kubectl exec dropped connection scrollback lost tmux inside pod workaround ok 11 20260802_120517_347
100 oh-my-posh OSC 133 shell integration windows terminal FTCS ok 19 20260802_120518_460
101 site:learn.microsoft.com Windows Hypervisor Platform optional feature Home edition ok 13 20260802_120556_553
102 Claude Code OpenTelemetry monitoring metrics events export ok 17 20260802_120641_113
103 Devin Cognition Labs Windows support machines parallel worktree ok 14 20260802_120803_430
104 tmux FAQ why not screen nicm design rationale ok 20 20260802_120810_755
105 undeadly.org tmux interview nicm 2007 terminal multiplexer ok 26 20260802_120817_404
106 Cursor background agents CLI Windows headless worktree API ok 14 20260802_120819_409
107 abduco dvtm brain-dump.org session management minimalism ok 22 20260802_120824_311
108 dtach Ned Crigler README detach terminal history ok 17 20260802_120831_557
109 Zed editor agent panel Windows native ACP external agents ok 19 20260802_120836_418
110 mosh Keith Winstein predictive local echo state synchronization protocol paper ok 14 20260802_120853_959
111 mosh windows client port Eternal Terminal alternative ok 10 20260802_120915_257
112 emacs vterm libvterm windows named pipe compile ok 18 20260802_120938_194
113 Factory Droid CLI Windows install download platform ok 21 20260802_120945_631
114 eat terminal emulator emacs eshell windows support akib ok 25 20260802_120953_863
115 zellij.dev blog why we built zellij session manager WASM plugin design ok 23 20260802_120954_753
116 Google Jules coding agent Windows local CLI worktree ok 23 20260802_121002_623
117 mitchellh.com libghostty embeddable terminal library blog ok 21 20260802_121006_866
118 jupyter kernel wire protocol ZeroMQ execute_request documentation ok 10 20260802_121014_722
119 tuimux libghostty rust multiplexer github ok 16 20260802_121015_486
120 langchain agent inbox human in the loop interrupt github ok 19 20260802_121033_382
121 coder boo libghostty embeddable terminal ok 20 20260802_121038_277
122 Evennia portal server split architecture reload persistent connections ok 26 20260802_121049_075
123 devpod kubernetes provider documentation loft-sh any cluster kubeconfig ok 14 20260802_121049_696
124 nREPL bencode protocol design clojure describe eval ops ok 16 20260802_121105_760
125 kind sigs k8s io windows without WSL docker desktop hyperv podman ok 20 20260802_121107_185
126 zellij.dev/news introducing zellij multiplexer discoverability imsnif ok 24 20260802_121110_239
127 SLIME swank protocol lisp emacs socket remote repl design ok 25 20260802_121149_270
128 Erlang remote shell erl -remsh hidden node epmd distributed documentation ok 7 20260802_121208_598
129 tmux structured output problem AI agent idle detection event stream github issue ok 21 20260802_121212_143
130 Source RCON protocol specification Valve wiki ok 27 20260802_121215_320
131 x3270 Paul Mattes 3270 terminal emulator session resume telnet ok 20 20260802_121232_164
132 Synchronet BBS FOSSIL driver door.sys multinode telnet native win32 ok 31 20260802_121245_779
133 mosh-windows-wrappers github mosh-client csharp port ok 19 20260802_121302_060
134 tmux is enough for AI agents you don't need a new multiplexer skeptic opinion ok 30 20260802_121325_840
135 Emacs ConPTY Windows native pty support subprocess NEWS ok 14 20260802_121506_393
136 VS Code Live Share terminal sharing collaborative session protocol architecture ok 32 20260802_121637_110
137 GNU screen savannah.gnu.org history Oliver Laumann 1987 origin ok 24 20260802_121657_823
138 "tmux control mode" terminal emulator client support -iTerm2 ok 21 20260802_121731_047
139 Pharo Smalltalk remote image debugging TelePharo headless server primary ok 16 20260802_121859_932
140 clink cmd.exe OSC 133 shell integration marks ok 17 20260802_122024_760
141 AttachConsole ReadConsoleOutput read another process console screen buffer ConPTY ok 22 20260802_122433_357
142 Docker Desktop Hyper-V backend deprecated removed Windows release notes ok 16 20260802_122455_829
143 LCOW Linux Containers on Windows deprecated removed Docker moby ok 23 20260802_122525_160
144 "Windows Hypervisor Platform" optional feature Windows 11 Home edition available ok 15 20260802_122636_069
145 DevPod loft-sh deprecated maintenance status 2026 successor ok 19 20260802_122743_275
146 Oliver Laumann screen 1987 comp.sources.unix original announcement window manager terminals ok 14 20260802_122759_263
147 DOOR32.SYS dropfile specification socket handle telnet BBS ok 10 20260802_123245_354
148 undeadly.org tmux interview Nicholas Marriott ok 16 20260802_125426_751
149 laurentkempe psmux windows terminal multiplexer copilot cli worktrees ok 14 20260802_125845_609

github-search — 53 retrievals (36 returned content)

# Query or URL Status Results When (UTC)
1 ORCA agentic development environment empty 0 20260729_202604_716
2 terminal multiplexer windows conpty detach reattach session empty 0 20260729_202605_352
3 cross-platform pty library conpty windows unix empty 0 20260729_202606_186
4 conpty ok 50 20260729_202613_356
5 terminal multiplexer ok 50 20260729_202615_853
6 pty ok 50 20260729_202618_041
7 orca ade ok 3 20260729_202643_657
8 agentic development environment ok 30 20260729_202645_411
9 psmux ok 40 20260729_205220_963
10 winsmux ok 3 20260729_205221_875
11 overmind ok 50 20260729_205224_910
12 hivemind ok 50 20260729_205227_551
13 mcp terminal server ok 25 20260729_205436_431
14 wexpect ok 7 20260729_205437_549
15 ttyd ok 50 20260729_205439_997
16 wetty ok 50 20260729_205442_884
17 pmux windows empty 0 20260729_205454_795
18 session daemon named pipe windows detach empty 0 20260729_205455_504
19 lg_termX himtdl windows mcp empty 0 20260729_205457_430
20 atch dtach alternative empty 0 20260729_205509_643
21 cmux parallel coding agents worktrees empty 0 20260729_205729_152
22 agent-client-protocol zed-industries empty 0 20260802_115312_001
23 agent client protocol error 0 20260802_115315_667
24 agent-client-protocol ok 50 20260802_115332_134
25 gotty ok 30 20260802_115339_883
26 sshx ok 30 20260802_115342_555
27 tmate ok 30 20260802_115345_155
28 upterm ok 30 20260802_115347_434
29 teleport ok 30 20260802_115349_436
30 code-server ok 30 20260802_115351_139
31 coder ok 30 20260802_115353_208
32 gitpod ok 30 20260802_115354_913
33 eclipse-che ok 30 20260802_115356_980
34 theia ok 30 20260802_115358_677
35 butterfly ok 30 20260802_115400_603
36 shellinabox ok 30 20260802_115402_525
37 webssh ok 30 20260802_115404_275
38 guacamole ok 30 20260802_115406_223
39 jupyter_server_terminals ok 8 20260802_115407_434
40 coder/mux error 0 20260802_115409_061
41 agent ok 8 20260802_115610_008
42 agents ok 50 20260802_115625_179
43 mux acp bridge coder error 0 20260802_115750_466
44 acp bridge terminal multiplexer error 0 20260802_115757_513
45 coder mux error 0 20260802_115815_767
46 coder mux acp empty 0 20260802_115823_073
47 claude-code acp support empty 0 20260802_120045_042
48 evennia ok 50 20260802_121510_031
49 nrepl ok 50 20260802_121514_408
50 bash_kernel ok 50 20260802_121517_765
51 terminal recorder asciicast windows conpty empty 0 20260802_122347_708

web-scraper — 193 retrievals (175 returned content)

# Query or URL Status Results When (UTC)
1 https://docs.claude.com/en/docs/claude-code/hooks ok 20260729_200606_607
2 https://docs.claude.com/en/api/agent-sdk/typescript ok 20260729_200625_969
3 https://docs.claude.com/en/docs/claude-code/costs ok 20260729_200627_721
4 https://docs.claude.com/en/docs/claude-code/settings ok 20260729_200651_501
5 https://docs.claude.com/en/api/agent-sdk/slash-commands ok 20260729_200655_236
6 https://github.com/reubenlavin08/cc-discord-remote ok 20260729_205251_003
7 openai/codex#25415 ok 20260729_205321_808
8 microsoft/vscode#201029 ok 20260729_205324_086
9 https://learn.microsoft.com/en-us/windows/console/attachconsole ok 20260729_205500_008
10 https://learn.microsoft.com/en-us/windows/console/attaching-to-a-console ok 20260729_205502_343
11 https://devblogs.microsoft.com/commandline/windows-command-line-introducing-the-windows-pseudo-console-conpty/ ok 20260729_205504_761
12 https://systemadministration.net/atch-the-small-tmux-alternative-that-bets-on-simple-sessions-and-persistent-logs/ error 20260729_205514_177
13 microsoft/terminal#5468 ok 20260729_205550_739
14 https://getfresh.dev/ ok 20260729_205627_338
15 https://docs.warp.dev/reference ok 20260729_205923_147
16 https://docs.warp.dev/getting-started/download error 20260729_205933_735
17 https://github.com/tmux/tmux/wiki/Control-Mode ok 20260802_115335_746
18 https://iterm2.com/documentation-tmux-integration.html ok 20260802_115338_704
19 tmux/tmux#1643 ok 20260802_115341_180
20 https://github.com/agentclientprotocol/agent-client-protocol ok 20260802_115400_938
21 https://github.com/microsoft/intelligent-terminal ok 20260802_115403_809
22 https://agentclientprotocol.com/protocol/v1/terminals ok 20260802_115429_895
23 https://zed.dev/blog/bring-your-own-agent-to-zed ok 20260802_115433_623
24 https://agentclientprotocol.com/protocol/v1/overview ok 20260802_115438_111
25 https://zed.dev/docs/ai/external-agents ok 20260802_115443_710
26 https://learn.microsoft.com/en-us/windows/win32/services/interactive-services ok 20260802_115449_948
27 https://superuser.com/questions/1860661/for-openssh-on-windows-what-is-an-alternative-to-nohup-screen-tmux ok 20260802_115452_842
28 https://opencode.ai/docs/server/ ok 20260802_115542_297
29 https://github.com/jupyter/terminado ok 20260802_115546_277
30 https://microsoft.github.io/debug-adapter-protocol/specification.html ok 20260802_115552_928
31 https://raw.githubusercontent.com/microsoft/node-pty/main/src/win/conpty_console_list.cc error 20260802_115607_605
32 https://github.com/microsoft/node-pty/blob/main/src/win/conpty_console_list.cc ok 20260802_115619_727
33 https://microsoft.github.io/debug-adapter-protocol/ ok 20260802_115628_683
34 pywinauto/pywinauto#492 ok 20260802_115644_613
35 https://raw.githubusercontent.com/jupyter-server/jupyter_server_terminals/main/jupyter_server_terminals/rest-api.yml error 20260802_115646_141
36 https://www.stepcodex.com/en/issue/codex-desktop-windows-attachconsole-failed-uncaught error 20260802_115648_181
37 https://raw.githubusercontent.com/jupyter-server/jupyter_server_terminals/main/jupyter_server_terminals/handlers.py error 20260802_115649_474
38 https://docs.github.com/en/actions/how-tos/manage-runners/self-hosted-runners?platform=windows ok 20260802_115652_459
39 https://learn.microsoft.com/en-us/windows/console/writeconsoleoutput ok 20260802_115653_406
40 https://depot.dev/blog/github-actions-runner-architecture-part-1-the-listener ok 20260802_115656_605
41 https://learn.microsoft.com/en-us/windows/console/classic-vs-vt ok 20260802_115717_916
42 https://learn.microsoft.com/en-us/windows/console/writeconsoleinput ok 20260802_115722_415
43 https://docs.docker.com/desktop/setup/install/windows-install/ ok 20260802_115741_198
44 https://learn.microsoft.com/en-us/virtualization/windowscontainers/manage-containers/hyperv-container ok 20260802_115744_245
45 https://unix.stackexchange.com/questions/189805/what-terminal-emulators-support-tmux-control-mode ok 20260802_115746_627
46 https://docs.docker.com/desktop/features/wsl/ ok 20260802_115746_633
47 https://xenospectrum.com/en/superlogical-terminal-state-multiplexer/ ok 20260802_115749_450
48 https://unrealcontainers.com/docs/concepts/windows-containers ok 20260802_115750_359
49 https://buildkite.com/docs/agent ok 20260802_115753_332
50 https://docs.asciinema.org/manual/asciicast/v2/ ok 20260802_115812_099
51 https://contour-terminal.org/vt-extensions/osc-133-shell-integration/ ok 20260802_115816_306
52 https://code.visualstudio.com/docs/terminal/shell-integration ok 20260802_115819_949
53 https://sw.kovidgoyal.net/kitty/shell-integration/ ok 20260802_115832_307
54 https://code.claude.com/docs/en/claude-directory ok 20260802_115837_636
55 https://github.com/Watfaq/PowerSession-rs ok 20260802_115841_320
56 https://github.com/nbedos/termtosvg ok 20260802_115844_082
57 https://learn.chatgpt.com/docs/non-interactive-mode ok 20260802_115859_314
58 https://github.com/dagger/container-use ok 20260802_115902_592
59 https://mitchellh.com/writing/superlogical ok 20260802_115906_530
60 https://github.com/anthropic-experimental/sandbox-runtime ok 20260802_115906_733
61 https://ghostty.org/docs/features ok 20260802_115909_400
62 https://code.claude.com/docs/en/sandbox-environments ok 20260802_115909_599
63 https://docs.docker.com/reference/cli/docker/container/attach/ ok 20260802_115917_333
64 https://kubernetes.io/docs/tasks/debug/debug-application/get-shell-running-container/ ok 20260802_115921_157
65 https://coder.com/blog/run-windows-development-environments-with-coder error 20260802_115925_122
66 https://docs.rancherdesktop.io/getting-started/installation/ ok 20260802_115929_346
67 https://invisible-island.net/xterm/ctlseqs/ctlseqs.html ok 20260802_115935_408
68 https://docs.openhands.dev/openhands/usage/architecture/runtime ok 20260802_120013_004
69 https://github.com/daaain/claude-code-log ok 20260802_120018_052
70 anthropics/claude-code#24365 ok 20260802_120019_420
71 https://opencode.ai/docs/troubleshooting/ ok 20260802_120021_196
72 https://modelcontextprotocol.io/seps ok 20260802_120022_566
73 https://github.com/ramtinj95/opencode-replay ok 20260802_120024_709
74 https://minikube.sigs.k8s.io/docs/drivers/hyperv/ ok 20260802_120045_943
75 https://minikube.sigs.k8s.io/docs/drivers/ ok 20260802_120048_476
76 https://docs.k3s.io/installation/requirements ok 20260802_120051_492
77 https://openai.com/index/unlocking-the-codex-harness/ ok 20260802_120101_689
78 https://docs.asciinema.org/getting-started/ ok 20260802_120136_436
79 https://docs.asciinema.org/manual/server/streaming/ ok 20260802_120141_985
80 https://learn.microsoft.com/en-us/powershell/module/microsoft.powershell.host/start-transcript ok 20260802_120145_731
81 https://docs.docker.com/ai/sandboxes/ ok 20260802_120153_270
82 https://docs.docker.com/ai/sandboxes/get-started/ ok 20260802_120155_927
83 https://github.com/dagger/container-use/releases ok 20260802_120159_441
84 dagger/container-use#159 ok 20260802_120202_538
85 https://github.com/docker/sbx-releases ok 20260802_120248_780
86 https://gitlab.freedesktop.org/Per_Bothner/specifications/blob/master/proposals/semantic-prompts.md ok 20260802_120309_227
87 https://aoagents.dev/docs/plugins/agents error 20260802_120351_070
88 https://gitlab.freedesktop.org/Per_Bothner/specifications/-/raw/master/proposals/semantic-prompts.md error 20260802_120356_207
89 https://code.visualstudio.com/docs/devcontainers/containers ok 20260802_120426_537
90 https://docs.imbue.com/readme.md error 20260802_120429_397
91 https://devpod.sh/docs/tutorials/docker-provider-via-wsl ok 20260802_120434_061
92 https://docs.docker.com/engine/logging/drivers/json-file/ ok 20260802_120437_743
93 https://learn.microsoft.com/en-us/windows/terminal/tutorials/shell-integration ok 20260802_120532_183
94 https://www.jetbrains.com/help/air/quick-start-with-air.html ok 20260802_120558_928
95 https://github.com/imbue-ai/sculptor ok 20260802_120619_967
96 https://kstack.sh/reference/skills/exec ok 20260802_120621_968
97 https://emdash.sh/docs/providers ok 20260802_120631_833
98 https://code.claude.com/docs/en/monitoring-usage ok 20260802_120654_027
99 https://undeadly.org/cgi?action=article;sid=20090712190402 ok 20260802_120852_719
100 https://www.brain-dump.org/blog/abduco-dvtm-a-lightweight-alternative-to-tmux-and-screen/ ok 20260802_120856_141
101 https://www.brain-dump.org/projects/abduco/ ok 20260802_120859_652
102 https://github.com/crigler/dtach/blob/master/README ok 20260802_120904_083
103 https://raw.githubusercontent.com/crigler/dtach/master/README error 20260802_120929_015
104 https://github.com/tmux/tmux/wiki/FAQ ok 20260802_120931_111
105 https://github.com/dagger/container-use/blob/main/LICENSE ok 20260802_121030_858
106 https://learn.microsoft.com/en-us/windows-hardware/drivers/dashboard/windows-hypervisor-platform error 20260802_121034_915
107 https://github.com/coder/boo ok 20260802_121100_633
108 https://mitchellh.com/writing/libghostty-is-coming ok 20260802_121103_078
109 https://zellij.dev/news/session-manager-protobuffs/ ok 20260802_121105_767
110 https://zellij.dev/news/ ok 20260802_121131_973
111 https://github.com/zellij-org/zellij ok 20260802_121135_262
112 https://rmux.io/ ok 20260802_121158_073
113 https://charliek.github.io/roost/ ok 20260802_121201_188
114 https://github.com/Helvesec/rmux ok 20260802_121204_472
115 https://www.conductor.build/docs/concepts/git-worktrees ok 20260802_121228_698
116 awslabs/cli-agent-orchestrator#182 ok 20260802_121242_649
117 https://nikiforovall.blog/ai/2026/06/27/tmux-message-bus.html ok 20260802_121245_184
118 https://github.com/OEN-Tech/tmuxai ok 20260802_121248_740
119 https://github.com/operonlab/tmux-agent-status/blob/main/docs/detection-matrix.md ok 20260802_121251_110
120 https://www.gitkraken.com/kepler ok 20260802_121256_420
121 https://www.brain-dump.org/talks/abduco-dvtm-cosin18.pdf error 20260802_121315_912
122 https://github.com/Nopm/WinMosh ok 20260802_121344_209
123 https://qht.co/item?id=49098965 error 20260802_121344_930
124 https://www.evennia.com/docs/latest/Components/Portal-And-Server.html ok 20260802_121347_005
125 https://spec.nrepl.org/ ok 20260802_121349_841
126 https://jupyter-client.readthedocs.io/en/stable/messaging.html ok 20260802_121356_007
127 https://github.com/langchain-ai/agent-inbox/blob/main/README.md ok 20260802_121359_730
128 https://elpa.nongnu.org/nongnu-devel/doc/eat.html ok 20260802_121405_480
129 https://guancyxx.cn/en/blog/tmux-skills-ai-agents ok 20260802_121426_074
130 https://news.ycombinator.com/item?id=49098965 ok 20260802_121437_971
131 https://www.erlang.org/doc/system/distributed.html ok 20260802_121612_904
132 https://web.archive.org/web/20250614151801/https://mosh.org/ ok 20260802_121620_985
133 https://github.com/ekzhang/sshx ok 20260802_121744_787
134 https://github.com/owenthereal/upterm ok 20260802_121747_996
135 https://debbugs.gnu.org/cgi/bugreport.cgi?bug=71472 ok 20260802_121902_921
136 https://codeberg.org/akib/emacs-eat ok 20260802_121906_185
137 https://docs.docker.com/subscription/desktop-license/ ok 20260802_121959_024
138 https://learn.microsoft.com/en-us/windows/win32/winstation/window-station-and-desktop-creation ok 20260802_122030_458
139 https://podman-desktop.io/docs/installation/windows-install ok 20260802_122035_440
140 https://ohmyposh.dev/docs/configuration/general ok 20260802_122046_777
141 https://docs.openhands.dev/openhands/usage/windows-without-wsl error 20260802_122052_770
142 https://docs.openhands.dev/openhands/usage/local-setup ok 20260802_122055_406
143 https://docs.podman.io/en/latest/markdown/podman-machine-init.1.html ok 20260802_122105_866
144 https://podman.io/docs/installation ok 20260802_122108_269
145 https://docs.docker.com/ai/sandboxes/usage/ ok 20260802_122113_099
146 https://learn.microsoft.com/en-us/windows-server/administration/windows-commands/tscon ok 20260802_122128_627
147 https://learn.microsoft.com/en-us/answers/questions/5789906/scheduler-tasks-with-security-options-run-whether ok 20260802_122132_429
148 https://github.com/containers/podman/blob/main/docs/tutorials/podman-for-windows.md ok 20260802_122133_912
149 https://kind.sigs.k8s.io/docs/user/quick-start/ ok 20260802_122318_089
150 https://code.claude.com/docs/en/headless ok 20260802_122318_622
151 https://code.claude.com/docs/en/cli-reference ok 20260802_122354_635
152 https://learn.microsoft.com/en-us/virtualization/hyper-v-on-windows/quick-start/enable-hyper-v ok 20260802_122419_168
153 https://learn.microsoft.com/en-us/virtualization/api/ ok 20260802_122421_869
154 https://docs.docker.com/engine/deprecated/ ok 20260802_122548_758
155 https://learn.microsoft.com/en-us/virtualization/api/hypervisor-platform/hypervisor-platform ok 20260802_122656_072
156 https://docs.docker.com/desktop/features/kubernetes/ ok 20260802_122728_091
157 https://mosh.org/ error 20260802_122835_767
158 https://www.evennia.com/docs/latest/Setup/Installation.html ok 20260802_122837_949
159 https://x3270.miraheze.org/wiki/X3270if ok 20260802_122944_099
160 https://x3270.bgp.nu/ ok 20260802_122947_355
161 https://x3270.miraheze.org/wiki/Ws3270 error 20260802_123018_111
162 https://x3270.miraheze.org/wiki/HTTP_interface error 20260802_123019_684
163 https://x3270.miraheze.org/wiki/Scripting ok 20260802_123021_327
164 https://nrepl.org/nrepl/design/transports.html ok 20260802_123110_452
165 https://wiki.synchro.net/ref:door32.sys ok 20260802_123120_063
166 https://developer.valvesoftware.com/wiki/Source_RCON_Protocol ok 20260802_123122_776
167 https://wiki.synchro.net/ref:fossil ok 20260802_123154_955

The Terminal Middle Layer: A Map of the Space

0. What this document is, and how to read it

0.1 The problem

An ADE (Agentic Development Environment — a desktop app that spawns coding-agent CLIs, one per git worktree, each in its own terminal pane) owns the PTY today. When the ADE dies, restarts, or gets replaced, every agent session dies with it. ADEs churn fast: this census records six that died or closed in the last eight months.

The thing being shopped for is a layer that sits under the shell and above the agent, owned by neither:

   ADE layer         ORCA today, a different ADE tomorrow — deliberately swappable
      |  spawns via an overridable launch command
   MIDDLE LAYER      <-- the subject of this document
      |  owns the PTY; exposes an ADE-independent interface
   agent CLI         claude, codex, opencode, copilot …

ORCA (stablyai/orca, Electron + node-pty) is the current ADE. Its per-agent launch command is overridable — agentCmdOverrides [SRC src/shared/types.ts, consumed at src/shared/tui-agent-launch-command.ts:30] — so the hook point for inserting a middle layer exists and is verified.

The four hard constraints, applied as pass/fail throughout:

# Constraint What fails it
1 Native Windows 11, no WSL Anything POSIX-only, WSL-required, or Linux-container-only
2 Open source Commercial/closed tools are reported as context, clearly labelled, never as candidates
3 Cross-platform is a plus, not a requirement Windows-only is a demerit, not a disqualifier (the user also runs Linux)
4 Headless-first Anything that needs a human attached, or a GUI, to be useful

Plus one topology requirement that is not a constraint but kills candidates anyway: nestability. The middle layer runs inside the ADE's PTY, so prefix-key collisions, resize propagation, alt-screen handling, mouse passthrough and double VT parsing all matter.

Research date: 2026-08-02. Star counts and last-activity dates churn weekly — treat every number as a 2026-08-02 snapshot.

0.2 Table of contents

Section What is in it
1. Verdict The three findings, in one page
2. The Taxonomy Ten axes for placing any tool, present or future, plus a placement matrix and a worked example
3. The Full Inventory Every candidate found, viable and refuted — 3.1 native-Windows daemons, 3.2 POSIX ancestors, 3.3 PTY-as-a-service, 3.4 ADEs, 3.5 containers, 3.6 protocols, 3.7 PTY libraries, 3.8 non-multiplexer channels
4. Middle-Layer Fitness Candidates scored on the five things that matter. 4.1 is the build spec for Windows and 4.2 is the nesting hazard; if you read two subsections, read those
5. The Interface Options Map Seven interface shapes; 5.1 is the verb set a middle layer must expose
6. Negative Results and Dead Ends ConPTY facts that mislead, node-pty defects live in ORCA today, why late-attach is refuted
7. What Was Refuted 50 claims killed during research — do not re-import any of them
8. Open Questions 24 questions with a next step, a cost, and what the answer changes
9. Coverage Gaps What is not verified, ordered by how much damage a wrong assumption does
10. How to narrow this yourself A decision scaffold keyed on your answers, not mine
11. If you decide to build it The four places in this document that together are most of a build spec

0.3 Glossary

Terminal-emulator vocabulary used throughout, defined once.

Term Meaning
ADE Agentic Development Environment — a desktop app that spawns and supervises coding-agent CLIs, typically one per git worktree. ORCA, Zed, kandev, Pane, herdr are ADEs
PTY / pseudo-terminal A kernel object pair that lets one process pretend to be a terminal for another. On Unix it is /dev/ptmx; on Windows it is ConPTY
ConPTY Windows' pseudo-console API (Win10 1809+). CreatePseudoConsole returns an HPCON handle; the OS spawns a headless conhost.exe behind it that translates between VT byte streams and the classic Windows console API
HPCON The opaque handle identifying one ConPTY instance. Whoever holds it is the only party that can resize it
alt-screen The secondary screen buffer (CSI ?1049h) that full-screen TUIs switch into so they can restore the scrollback on exit. Nesting two programs that both use it produces visible corruption if either mishandles the switch
DA1 "Device Attributes, primary" — the CSI c query a terminal is expected to answer with a capability report. ConPTY sends one at startup and blocks for up to 3 s waiting for a reply
CPR / ESC[6n "Cursor Position Report" — a query asking the terminal where the cursor is. ConPTY emits one at startup when INHERIT_CURSOR is set, and will not service the child's console connection until it is answered
XTWINOPS CSI 18 t An xterm query meaning "how many rows and columns are you?", answered with CSI 8 ; rows ; cols t. The only in-band way to learn a size when there is no SIGWINCH
OSC 133 / OSC 9;4 / OSC 777 Operating System Command escapes carrying semantic metadata rather than glyphs: 133 marks prompt/command/output boundaries, 9;4 sets Windows taskbar progress, 777 is a notification channel
win32-input-mode An in-band negotiated mode (CSI ?9001h) in which a ConPTY-hosted child receives full Win32 INPUT_RECORD key fidelity encoded as VT, instead of lossy VT key encoding. Negotiated at runtime, not a creation flag — see 6.1
integrity level The Windows mandatory-access-control tier of a process (Low / Medium / High / System). A lower-integrity process cannot read a higher-integrity console
session 0 The Windows terminal-services session reserved for services. Interactive logons get session 1+. Console objects do not cross the boundary
-CC control mode tmux's line-based machine protocol: you send tmux commands, it replies with %begin/%end/%output/%layout-change lines. Lets an outer program own rendering while tmux owns sessions. Fully described in 3.6
double parse Two VT parsers in series, each rendering and re-emitting: multiplexer parses the agent's bytes into a grid, re-emits ANSI, ADE parses that again. Costs fidelity at every hop. Section 2.2 shows it is avoidable
L0-L3 Persistence levels defined in 2.9: 0 = dies with the connection, 1 = survives client disconnect, 2 = survives the ADE, 3 = survives a reboot

0.4 Evidence tiers

Every claim carries its evidence tier, separated from judgement. An untagged cell is not verified — it is inherited from a row note, a neighbouring citation, or (in the tables that predate this convention) simply unverified. Do not read absence of a tag as a stronger claim than the weakest tag present.

Tag Meaning
[SRC] Verified by reading source code or a config/manifest file
[ART] Verified by release-artifact list, CI config, or package manifest
[DOC] Verified against a project's own primary documentation
[2ND] Secondary source only — blog, aggregator, third-party writeup, field report
[EMP] Verified empirically on this machine (native Windows 11, build 26200)
[INF] Inference from verified facts — reasoning, not observation
[UNV] Asserted somewhere, not verified in any pass
Judgement: Reading of the evidence, not the evidence. Always labelled

0.5 Verdict vocabulary

The Verdict column in every section-3 table uses exactly these words. Each has a test.

Verdict Test it passed / failed
VIABLE Meets all four hard constraints, is installable or vendorable today, and every load-bearing claim was verified at source
VIABLE-WITH-CAVEAT As above, but one named property is degraded or unproven. The caveat is stated inline — read it, it is always the deciding factor
EXTRACTABLE Works and is verified, but is not shipped as a standalone product. You vendor the package or run the host binary
UNVALIDATED Design is sound and source-verified, but nobody outside the author has run it on Windows. Zero external issues, zero external CI, or zero users
REFUTED Fails a hard constraint. The failing constraint is always named
CONTEXT Not a candidate. Read it for a design idea, a precedent, or a warning
DEAD Archived, sunset, or abandoned by its own maintainers
NEGATIVE SPACE A thing that does not exist. Recorded so it is not searched for again

Note the previous editions of this document used ~18 different verdict strings; UNVERIFIED and REFUTED (no external validation) have both been normalised to UNVALIDATED, and VIABLE (extract, not install) / VIABLE (vendor the package) to EXTRACTABLE. One status per candidate, stated once in 3.x, propagated to 1 and 4.

0.6 Roster — the names, before they arrive cold

Every tool named in section 1 or the taxonomy, with one clause and where it is covered. Read this once and section 2 becomes readable without jumping.

Name One clause Covered in
psmux Rust tmux-workalike, Windows-only, loopback TCP + tmux -CC control mode, 3.1k★ 3.1, 4, 4.2
Zellij Rust multiplexer, 34.6k★, Windows port shipped 0.44.0, named pipes, WASM plugins 3.1, 4
rmux Rust tmux-verb-compatible daemon, 2.5k★, named pipes scoped by SID + integrity level 3.1, 4
herdr Rust multiplexer/ADE hybrid, 23.5k★, Windows on the preview channel only 3.1, 4
oly (slaveOftime/open-relay) Rust session daemon, 89★, named-pipe IPC + HTTP/WS 3.1, 4
qscreen Rust single-pane session daemon, 6★, structured frame protocol + raw-byte attach 3.1, 4
quil Go session daemon, 11★, MCP server with 18 tools, ConPTY but AF_UNIX IPC 3.1, 4
wezterm-mux-server The headless multiplexer inside WezTerm (28k★); 15 CLI verbs, versioned binary codec 3.1, 4
OpenCode /pty An undocumented HTTP+WebSocket PTY API inside OpenCode (192k★) 3.3, 3.6, 4
ao pty-host A hidden detached-ConPTY subcommand inside Agent Orchestrator (8.7k★) 3.1, 3.4, 4
upterm Go session-sharing host over SSH, real ConPTY, but no detach/reattach 3.1, 4
boo (coder/boo) Zig screen-replacement on libghostty-vt with peek --json/wait --idle; POSIX-only 3.2
ht (andyk/ht) Rust "wrap any binary with a terminal interface"; the shape to copy, POSIX-only 3.3
ripple (yotsuda) C# MCP + ConPTY session tool; cited only for its VtLiteState and a post-mortem doc 2.2, 7.1
tuios, dtach, abduco/dvtm, reptyr POSIX-only ancestors and refuted candidates, kept for their design lessons 3.2
ORCA The ADE in use. Electron + node-pty. agentCmdOverrides is the hook point 3.4

1. Verdict

The middle layer already exists — ten times over — and not one of them is packaged as the thing you want.

Source-level verification found ten projects that meet the structural predicate: they run natively on Windows over ConPTY, they survive the client that spawned them dying, and they expose a non-GUI interface a third party can drive. Seven are standalone daemons:

Project Transport Status (see 0.5)
psmux loopback TCP + tmux -CC control mode VIABLE-WITH-CAVEAT — Windows-only, 8 months old
Zellij named pipes via the interprocess crate, Windows port shipped in 0.44.0 VIABLE
rmux named pipes, namespaced per SID + integrity level VIABLE
herdr named pipes, 23.5k stars VIABLE-WITH-CAVEAT — Windows on the preview channel only
oly/open-relay named pipes + HTTP/WS VIABLE-WITH-CAVEAT — unauthenticated pipe, no ACL
qscreen named pipes, raw-byte attach mode UNVALIDATED — 6 stars, zero CI, zero external users
quil AF_UNIX on all platforms, not named pipes — the "Named Pipes on Windows" claim in its own ADR-2 was never implemented UNVALIDATED — zero Windows CI

Three more exist inside larger products and are EXTRACTABLE rather than installable: wezterm-mux-server (headless by design, versioned binary codec, 15 CLI verbs, first-class attach/detach), OpenCode's /pty HTTP+WebSocket API (replay cursor, ticket auth, shipped in the Windows binary, documented nowhere on its own website), and Agent Orchestrator's ao pty-host (detached ConPTY host over loopback TCP with scrollback replay and largest-client resize arbitration).

Note on the predicate. An earlier edition of this document framed the set as "non-POSIX-IPC". That framing was wrong and is retired: AF_UNIX is a real Windows transport since Windows 10 1803, so a UnixListener in source proves nothing about platform support either way — see 2.8. What separates these ten from everything else is ConPTY + survival + a non-GUI interface, nothing about the socket family. quil is in the set and uses AF_UNIX; both are true.

Judgement: there is no leader. Each candidate forfeits exactly one of {maturity, Windows-nativeness, standalone packaging, contract stability}. psmux is Windows-only and 8 months old. Zellij is the most mature but its Windows input path keys off TERM/WT_SESSION, which an ADE PTY may not set — it picks the native-console path, which is implemented and conservative, so this is a config check rather than a breakage (see Q3). WezTerm's mux server is the strongest engineering but its newest tagged release is 2024-02-03, two and a half years old — the current Windows path is the rolling nightly tag. OpenCode's /pty is undocumented and its wire is UTF-8 text, not raw bytes. ao pty-host is a Hidden: true subcommand inside an 8.7k-star ADE.

The second finding reframes the question. The middle layer may not need to own a PTY at all. claude -p --input-format stream-json --output-format stream-json is a full-duplex NDJSON channel over ordinary pipes, identical on Windows and Linux; Claude Code hooks are a write path (permissionDecision + updatedInput answers interactive prompts headlessly, and type: "http" hooks let a long-lived process you own be the endpoint — for the 13 events that support http; SessionStart and Setup do not); OpenTelemetry emits 34 named claude_code.* identifiers including claude_code.tool.blocked_on_user. PTY ownership buys exactly two things those cannot: exact on-screen state, and a human typing into a live session.

What was foreclosed. Anthropic closed both network-attach requests as not_planned (#24365, #6686) — do not bet on Claude Code growing this natively. Non-WSL container paths narrow to two open-source options (Podman hyperv, minikube --driver=hyperv). Late-attach via AttachConsole is a read-only escape hatch at best. No terminal-control standard exists — MCP has zero terminal SEPs among 41 Final ones.


2. The Taxonomy

This is the durable part. Place any future tool on these ten axes and you will know what it is without re-surveying. Each axis carries a How to determine it column — the concrete check that places a tool you have never seen, so the axes work on tools that do not exist yet.

Axes 2, 8, 9 and 10 replaced earlier formulations that turned out to be wrong. What was wrong with each, and the evidence that killed it, is recorded in 7.1.

Two navigation aids follow the axes: 2.11 places every live candidate on all ten axes in one matrix, and 2.12 walks a tool that is not in the census through the ten checks end to end.

2.1 Session/window coupling

Pole Meaning How to determine it Instances
Monolithic One binary owns session persistence AND pane/window layout One executable, and killing it loses both the layout and the sessions screen, tmux, Zellij, psmux, rmux, herdr, boo
Split Persistence and layout are separate, swappable programs The layout program appears as an argument to the session program, and can be replaced with any other command abduco + dvtm
Session-only No layout concept at all No split, new-window or select-pane verb exists anywhere in the CLI dtach, qscreen (single pane by design), upterm

Judgement: the split pole (abduco/dvtm) is the closest existing precedent for what you are building — a session layer that deliberately does not own rendering. [DOC brain-dump.org/blog/abduco-dvtm-a-lightweight-alternative-to-tmux-and-screen/] abduco's author states it directly: "these are two distinct features: window and session management shouldn't be intermingled". Both halves are dormant (abduco last commit 2020-04-30, dvtm 2021-03-06) and POSIX-only, so the idea travels but the code does not.

2.2 Terminal-state ownership — three poles, not two

The original formulation ("rendering multiplexer vs transparent byte-passer") was refuted. There is a third pole and most modern candidates sit in it.

Pole Behaviour How to determine it Instances
Opaque relay No screen model; forwards bytes verbatim; cannot redraw on reattach No VT/emulator crate in the dependency manifest at all dtach [DOC dtach README] — "dtach does not have a terminal emulation layer, and passes the raw output stream of the program to the attached terminals"
Full VT owner Parses every escape into a grid, re-emits rendered ANSI The live-path payload type is a string or a rendered-cell struct, not a byte slice screen, tmux, Zellij, psmux TUI mode, herdr, ripple's VtLiteState
Parallel model, raw live path Maintains a VT model for snapshot/resize/scrollback bookkeeping, but ships raw child bytes on the live path The live-path payload type carries bytes / Vec<u8> / []byte, and a separate VT model exists for snapshots qscreen AttachMode::Bytes [SRC crates/qscreen-protocol/src/lib.rs:56-61]; oly ServerMessage::Data { data: Vec<u8> } [SRC src/http/ws.rs:36-46]; quil PaneOutputPayload { Data []byte } [SRC internal/ipc/protocol.go:204-208]; psmux -CC %output (pane output ring, octal-escaped) [SRC src/server/mod.rs:1222-1227]

Why this matters for nesting. The third pole means the ADE's own xterm.js can be the only VT parser in the chain while the daemon still replays scrollback on reattach. The "unavoidable double parse" framing is false — it is avoidable today, with shipped interfaces, in three of the four small candidates.

Apply the same test to OpenCode /pty and it lands in the second pole, not the third. [SRC packages/core/src/pty/protocol.ts] its outbound frames are described as "raw UTF-8 terminal chunks", but the payload type is a JS string: chunks(data: string) slices at REPLAY_CHUNK = 64 * 1024 using String.slice, i.e. UTF-16 code units, so a surrogate pair straddling a replay-chunk boundary is split into lone surrogates; and decodeInput uses new TextDecoder("utf-8", { fatal: true }) inside a try { … } catch { return undefined }, so invalid UTF-8 on the input path is silently dropped — the file's own comment says so. This is the same failure class as psmux's String::from_utf8_lossy (4.2, third-order), and it is called out here for the same reason: applied consistently, the criterion demotes a candidate this document otherwise praises.

2.3 Client/server namespace model

Ordered worst to best. Note the poles are about who can collide with you, not about the transport.

Pole Mechanism How to determine it Instances
Machine-global default The default name contains no user, SID or project component, so two users on one host collide Read the socket/pipe name construction: is there any per-user or per-project component in the default path? oly — default open-relay.oly.sock, no user component [SRC src/config.rs:123-126]
Single fixed instance (per-user, no per-project knob) One instance per user by construction, no way to run two side by side The name embeds a user component but no -L-style label, and no env var overrides it qscreen (\\.\pipe\qscreen-<user>) [SRC crates/qscreen-shared/src/lib.rs:6-11]
Socket-name-as-namespace The socket/pipe name IS the namespace, selectable per invocation A -L/-S flag or a name env var exists tmux -L/-S [DOC tmux(1)], psmux -L/-S, rmux -L, oly via OLY_SOCKET_NAME [SRC src/config.rs:123-126]
Env-var home split A whole state directory per instance An env var relocates the entire state root, not just the socket quil QUIL_HOME, Claude Code CLAUDE_CONFIG_DIR
Identity-scoped Namespace derived from user SID + integrity level, not from a string the caller picks The name is computed from GetTokenInformation-class data, so it cannot be spoofed by argument rmux [SRC crates/rmux-ipc/src/endpoint.rs]: \\.\pipe\{prefix}-{sid}-il-{integrity}-{label}

Correction, applied to a judgement this document previously got wrong. oly's socket name is not hardcoded: [SRC src/config.rs:123-126] reads std::env::var("OLY_SOCKET_NAME").ok().and_then(normalize_optional_string).unwrap_or_else(|| "open-relay.oly.sock".to_string()) — an env-var-selectable name with a machine-global default. It therefore occupies the socket-name-as-namespace pole, same as tmux. Earlier editions placed it in a "None — no isolation knob" pole and called it the worst design found; the cited lines refute that. What survives, and is the real defect, is separate: the default name has no user component, and the pipe is created with no ACL and no FILE_FLAG_FIRST_PIPE_INSTANCE [SRC src/ipc.rs, 296 lines, zero ACL/SID/security-descriptor keywords], so a hostile local process can pre-create the pipe and harvest client connections. Namespacing and access control are different problems and oly fails the second one, not the first.

Judgement: identity-scoped is strictly the best design found and only rmux does it, because it is the only pole where the namespace cannot be entered by a process that merely guesses a string. Everything below it depends on the pipe's ACL for actual security, and only rmux and qscreen set one.

2.4 Command-language uniformity

Pole How to determine it Instances
One grammar usable from shell, config file, and keybinding The same verb string works as tool verb, as a config-file line, and as a keybinding target tmux [DOC tmux(1) "COMMAND PARSING AND EXECUTION"], psmux, rmux (90+ tmux-compatible verbs)
Separate keybinding table bolted onto a distinct config syntax The config file's grammar and the CLI's grammar do not overlap screen
Discoverability-first on-screen hinting The primary interface is a rendered hint bar, and CLI verbs are secondary Zellij [DOC README: "must not sacrifice simplicity for power"]

Why this axis matters for a headless middle layer: the first pole is the only one where a script, a hook and a human all speak the same language, so nothing has to be re-learned when the human leaves the loop.

2.5 Extensibility mechanism

Pole How to determine it Instances
Shell hooks / pipes Extension points take a shell command string tmux hooks, pipe-pane, screen backtick
External wire protocol for driving the multiplexer A documented framing exists that a non-child process can speak tmux -CC control mode, psmux -CC, qscreen ScreenFrame, WezTerm mux codec
Sandboxed compiled plugin runtime Plugins ship as compiled artifacts against a schema'd host API Zellij WASM plugins (protobuf-schema'd since 0.38.0) [DOC zellij.dev/news/session-manager-protobuffs/]
Embedded scripting runtime An interpreter is linked in and the config file is a program WezTerm (Lua config + event API)
Typed SDK A published client library exists in at least one language, with types rmux (Rust/Python/TypeScript: session/pane/snapshot/wait_for_text), herdr (third-party TS + Python clients)

2.6 Agent-state derivation strategy

Pole Mechanism Instances Failure mode
Vendor-emitted structured events The agent CLI itself emits typed events Claude Code OTel (34 claude_code.* identifiers) + JSONL transcripts + stream-json; Codex --json rollout events; OpenCode /global/event SSE Vendor-specific; config surface changes
Native typed state The multiplexer owns a VT parser and exposes typed peek/wait/snapshot boo wait --idle/peek --json, rmux snapshot(), quil MsgScreenshotPaneResp{Text,CursorX,CursorY}, andyk/ht Only as good as the emulator
External heuristic scraping Regex/OSC-title matching over another program's rendered screen tmux-agent-status, tmuxai, kiro_cli poller Documented convergent bug (see status caveat below): whole-buffer matches go stale when a TUI redraws in place [DOC awslabs/cli-agent-orchestrator#182 — "Kiro CLI 2.0 TUI redraws the screen in-place… retains 'Kiro is working' from earlier rendering alongside the new idle prompt"; impact: "Handoff delegations never complete"]. Independently mitigated the same way by two projects: anchor to bottom-N lines/OSC title, never whole-screen, plus NOT-gates for stale artifacts

How to determine which pole a tool is in: grep for a VT/emulator dependency and for a regex over screen text. Structured-event consumers have neither; native-typed-state tools have the emulator; scrapers have the regex and no emulator.

Citation status, disclosed per this document's own method rule 1. awslabs/cli-agent-orchestrator#182 is closed as completed on 2026-04-20 and its title begins fix(kiro_cli): — the bug was fixed. It remains valid evidence that the failure mode is real and was hit in production by a shipped project; it is not evidence that any current tool is broken today. Also disambiguate: awslabs/cli-agent-orchestrator (the AWS project, cited here) and Untrivial-ai/agent-orchestrator (called "Agent Orchestrator" everywhere else in this document, and the home of ao pty-host) are different, unrelated projects.

Judgement, with the headless-first constraint applied: scraping is disqualified as an architecture, on the grounds that a stale-buffer misread has nobody to notice it when no human is in the loop — not on the grounds that any specific instance is currently broken. Vendor-emitted structured events are the correct default channel and they cost nothing — they do not touch the PTY, so they survive ADE swaps for free.

On "34 events". There are exactly 34 distinct claude_code.* identifiers in the monitoring documentation, but they are not all events: the set mixes metrics (cost.usage, token.usage, session.count, lines_of_code.count), events, and spans (tool.execution, hook). The load-bearing one for headless use, claude_code.tool.blocked_on_user, is confirmed present. "34 named claude_code.* identifiers" is the accurate phrasing and is used from here on.

2.7 Substrate stance

Pole How to determine it Instances
Wrap tmux, keep it as substrate tmux appears as a hard prerequisite in install docs, or as a spawned binary in source tmuxai, tmux-agent-status, tmux-message-bus, claude-squad, uzi, Agent Orchestrator (on Darwin/Linux)
Replace tmux with a new PTY-owning binary A PTY library appears in the dependency manifest and no tmux prerequisite exists psmux, quil, oly, qscreen, zellij, rmux, herdr, boo
No multiplexer at all — no PTY No PTY library anywhere; the agent is spawned with ordinary pipes Claude Code -p --input-format stream-json; Codex app-server; OpenHands EventStream; GitHub Actions runner

The criterion that decides the pole, stated independently of any one writeup. A session layer is necessary exactly when the agent host does not own process lifecycle end to end. If the agent tool spawns, supervises and outlives its own processes, a multiplexer underneath is dead weight; if the host dies and takes its children with it, the multiplexer is the only thing that makes sessions survivable.

That criterion is not this document's invention — it is the same line drawn by an independent practitioner writeup. [2ND guancyxx.cn/en/blog/tmux-skills-ai-agents] Galen Guan: "For Claude Code and OpenClaw users working through SSH-disconnected sessions, tmux is essential infrastructure. For Hermes users, it's an unnecessary abstraction layer" — where Hermes is an agent tool whose own terminal tool owns process lifecycle end to end, and OpenClaw is an agent CLI that, like Claude Code, does not. (Tagged [2ND], not [DOC]: this is a third-party blog writing about somebody else's tools, not a project's own documentation. It corroborates the criterion; it does not establish it.)

Judgement: ORCA spawns agent CLIs through its own in-process node-pty, so sessions die with ORCA. It is on the "essential infrastructure" side of the line by construction. The wrapper is not NIH here.

2.8 Transport binding — four poles, not two

The original binary formulation ("POSIX-only vs native-Windows") misclassifies candidates. AF_UNIX is a real Windows transport since Windows 10 1803, so a UnixListener in source is no longer proof of a POSIX-only daemon.

Pole Test Instances
cfg(unix)-gated UDS / forkpty Source has std::os::unix::net, std.posix, forkpty, or nix with term features dtach, abduco, boo [SRC std.posix in 11 files], ht [SRC src/pty.rs forkpty + nix], tuios, gotty (creack/pty start_windows.go literally return nil, ErrUnsupported)
Cross-platform AF_UNIX shim UDS routed through a shim crate WezTerm [SRC wezterm-uds/src/lib.rs: #[cfg(windows)] use uds_windows::UnixStream], Codex [SRC codex-rs/uds/src/lib.rs:1 "Cross-platform async Unix domain socket helpers", line 162 #[cfg(windows)] → uds_windows::UnixListener::bind]
Windows named pipes \\.\pipe\, CreateNamedPipeW, interprocess::GenericNamespaced, tokio::net::windows::named_pipe oly, qscreen, zellij, rmux, herdr, node-pty (conin/conout)
TCP loopback TcpListener::bind("127.0.0.1:0") psmux (control + cross-session), ao pty-host, Warp local_control (http://127.0.0.1:PORT/v1/control), pywinpty's internal bridge
Naked AF_UNIX on all platforms net.Listen("unix", …) unconditional quil [SRC internal/ipc/server.go:311] — works on Windows 10 17063+, but os.Chmod(path, 0600) is a no-op on Windows and there is no SO_PEERCRED, so the socket's only protection is the parent directory ACL

The two AF_UNIX poles differ in hygiene, not in portability. Both work on Windows 10 17063+. The difference is that a shim crate documents the platform intent and centralises the fallbacks, whereas a naked net.Listen("unix", …) leaves the caller believing POSIX socket semantics still apply. Neither pole gets kernel-enforced permissions on Windows: SO_PEERCRED does not exist, and chmod/os.Chmod on an AF_UNIX path is a no-op. Whichever pole you are in, an AF_UNIX socket on Windows is protected only by the ACL on its parent directory. A properly-ACL'd named pipe is strictly stronger; a loopback TCP port is strictly weaker (see 5.1's closing note).

Cross-cutting fact worth remembering: the uds_windows Rust crate and the interprocess crate are the two de-facto answers. WezTerm and Codex independently landed on uds_windows; Zellij, herdr and oly independently landed on interprocess. [DOC wezterm docs/multiplexing.md: "Unix domains are supported on all systems, even Windows"]

2.9 Persistence scope

How to determine the level: kill the spawning process tree, then ask two questions — is the child still running, and can a new client re-attach to it? Both yes = L2. Child alive but no re-attach path = the tool is a supervisor, not a session host. Neither = L0.

Level Meaning Instances
0 — none New process per connection, killed on disconnect ttyd [SRC src/protocol.c:377-383 — LWS_CALLBACK_CLOSED unconditionally kills, per-connection pss->process], wetty [SRC src/server/spawn.ts .on('disconnect', () => term.kill())], terminado UniqueTermManager
1 — survives client disconnect Session lives as long as the host process upterm, sshx, terminado NamedTermManager, ACP session/load (conversation state, not process)
1+ — L1 plus cursor-resumable replay Survives client disconnect and a reconnecting client can resume from an absolute output cursor rather than replaying from zero. Still dies with the host process, so it is not L2 OpenCode /pty [SRC packages/core/src/pty/protocol.ts] — the only instance found
2 — survives the ADE Detached daemon outlives whoever spawned it psmux, quil, oly, qscreen, zellij [SRC zellij-client/src/lib.rs:463-480 CREATE_NO_WINDOW | CREATE_NEW_PROCESS_GROUP], rmux, ao pty-host [SRC conpty/spawn_windows.go — CREATE_NEW_PROCESS_GROUP + DETACHED_PROCESS "so the host survives daemon exit"], wezterm-mux-server --daemonize
3 — survives host reboot State restored across a full restart cmux (macOS only) [DOC FAQ: "the state survives a full computer restart… Agent sessions like Claude Code, Codex, and OpenCode come back too"] — the only level-3 claim found anywhere

2.10 Ownership timing — a settled question, recorded so it is not re-opened

This is not an axis: there is nothing to place, because one pole is refuted outright. It is kept in the taxonomy's numbering because every re-derivation of this research has re-opened it.

Pole Mechanism Verdict
Own-from-birth The wrapper spawns the child and holds the PTY handle The only viable pole. Every working implementation found does this
Late-attach Reach into a console the wrapper did not spawn (AttachConsole(pid)) Read-only escape hatch at best — the full argument is in 6.4

Correction that removes the last piece of prior art for late-attach: node-pty was thought to prove late-attach works. It does not. [SRC src/windowsPtyAgent.ts:149] this._innerPid = connect.pid; — the PID it passes to AttachConsole is the process it spawned itself. node-pty re-enters its own child's console from a disposable fork. It is another own-from-birth system. Nothing found anywhere late-attaches to a foreign console and drives it, except one hobby project (section 6.4).

2.11 The placement matrix — every live candidate on all ten axes

Rows are the candidates that survived to section 4. Read a column to compare a design decision across the field; read a row to characterise one tool. Cells are pole names, abbreviated; means the axis does not apply to that candidate's shape.

Candidate 2.1 coupling 2.2 state ownership 2.3 namespace 2.4 command lang 2.5 extensibility 2.6 agent state 2.7 substrate 2.8 transport 2.9 persist 2.10 timing
psmux Monolithic Full VT (TUI) / raw (-CC) Socket-name (-L/-S) One grammar Wire protocol (-CC) Native typed Replace TCP loopback L2 Own-from-birth
Zellij Monolithic Full VT owner Socket-name Hint-first WASM plugins Native typed Replace Named pipes L2 Own-from-birth
rmux Monolithic Full VT owner Identity-scoped One grammar Typed SDK Native typed Replace Named pipes L2 Own-from-birth
herdr Monolithic Full VT owner Socket-name One grammar Typed SDK (3rd-party) Native typed Replace Named pipes L2 (beta) Own-from-birth
oly Monolithic Parallel/raw Socket-name (OLY_SOCKET_NAME), machine-global default One grammar HTTP/WS Native typed Replace Named pipes L2 Own-from-birth
qscreen Session-only Parallel/raw Single fixed (per-user) One grammar Wire protocol Native typed Replace Named pipes L2 Own-from-birth
quil Monolithic Parallel/raw Env-var home (QUIL_HOME) One grammar MCP (18 tools) Native typed Replace Naked AF_UNIX L2 Own-from-birth
wezterm-mux-server Monolithic Full VT owner Socket-name (domains) One grammar Lua + wire codec Native typed Replace AF_UNIX shim L2 Own-from-birth
OpenCode /pty Session-only Full VT (UTF-8 string wire) — (HTTP, per-server) HTTP/WS Vendor events No multiplexer HTTP/WS L1+ Own-from-birth
ao pty-host Session-only Parallel/raw Registry file per session Binary wire protocol Vendor events Replace TCP loopback L2 Own-from-birth
upterm Session-only Opaque relay (SSH) — (SSH keys) One grammar Replace SSH L1 Own-from-birth
No-PTY architecture — (no VT at all) Env-var home (CLAUDE_CONFIG_DIR) CLI + NDJSON Hooks (http/mcp_tool) Vendor events No multiplexer pipes / HTTP you own it

2.12 Worked example — placing a tool that is not in the census

To show the procedure is mechanical, here is mobydeck/atch — refuted in 3.2 on one line, walked through all ten axes from scratch.

  1. Coupling — no split/new-window/select-pane verb in the CLI → session-only.
  2. State ownership — no VT or emulator crate in the manifest; it forwards the child's stream → opaque relay.
  3. Namespace — socket path is taken from an argument → socket-name-as-namespace.
  4. Command language — one small verb set, no config-file grammar → one grammar, trivially.
  5. Extensibility — none published → no pole.
  6. Agent state — nothing typed exposed → no pole; a consumer would have to scrape.
  7. Substrate — owns its own PTY, no tmux prerequisite → replace.
  8. Transport — POSIX sockets, cfg(unix)-shaped, no Windows path → cfg(unix)-gated UDS. This is the disqualifying cell.
  9. Persistence — detached daemon, re-attachable → L2.
  10. Timing — spawns its own child → own-from-birth.

Verdict: REFUTED, on constraint 1, decided entirely by axis 8. Two useful properties of this procedure show up here: the verdict falls out of a single cell rather than a holistic impression, and the reason is recorded in a form the next reader can re-check in one grep. Every REFUTED row in section 3 was reached this way.


3. The Full Inventory

3.1 Session daemons and multiplexers — native-Windows candidates

Reading the Last activity column: every value is the repo's pushed_at unless the cell says otherwise. Where a project's default branch and its pushed_at disagree (side branches), both are given.

Reading the Linux/macOS column (constraint 3): this is the "uniform Windows+Linux behaviour is a plus" check, and it changes the reading of three rows at a glance.

Name Runtime License Stars Last activity Native Win Linux/macOS Interface Persist Nesting Verdict Evidence
psmux/psmux Rust daemon; a PowerShell installer/test suite is the larger byte count, so "mostly PowerShell" language stats are misleading — the product is the Rust binary MIT 3,140 2026-08-02 Yes [SRC crates/portable-pty-psmux] No — Windows-only. [ART psmux/psmux .github/workflows/ci.yml; release v3.3.7 ships six Windows-only assets] CI builds only x86_64/i686/aarch64-pc-windows-msvc; the ubuntu-latest/macos-latest job is posix-helper-tests, running two shell scripts, not a build tmux CLI verbs + -C/-CC control mode L2 Own guard + PSMUX_ALLOW_NESTING=1 override; pipe mode negotiates size via CSI 18 t VIABLE-WITH-CAVEAT (Windows-only; unauthenticated cross-session port) [SRC src/main.rs:232-241, 4031-4032, 4194; src/control.rs; docs/control-mode.md (432 lines)]
zellij-org/zellij Rust MIT 34,647 2026-08-01 Yes [SRC zellij-server/src/os_input_output_windows.rs — CreatePseudoConsole/ResizePseudoConsole/HPCON; zellij-utils/src/ipc.rs:8 interprocess::local_socket] Yes — original target platforms CLI + WASM plugins L2 nested_session_handling = ask/fullscreen/descend/never [SRC zellij-utils/assets/config/default.kdl:268-275] VIABLE [ART v0.44.3 ships zellij-x86_64-pc-windows-msvc.zip + .msi]
Helvesec/rmux Rust (12 crates, 21 MB) dual-licensed; the pair is not stated in repo metadata (GitHub reports NOASSERTION) — read LICENSE-* at the tag you would vendor before relying on it 2,533 2026-07-26 Yes [SRC crates/rmux-ipc/src/stream_windows.rs — NamedPipeServer + ImpersonateNamedPipeClient + server_identity_windows.rs] Yes 90+ tmux-compatible verbs + Rust/Python/TS SDKs + Ratatui widget + E2E-encrypted web share L2 Not tested VIABLE [ART rmux-0.9.1-windows-x86_64.zip; winget/scoop/choco]
herdrdev/herdr Rust Apache-2.0 23,466 2026-08-01 Beta only [DOC website/src/content/docs/windows-beta.mdx] Yes — stable v0.7.5 ships linux + macos assets and no Windows asset CLI + HTTP API (src/api/server.rs) + third-party TS/Python SDKs L2 (beta — "Local persistent sessions | beta" is listed under Supported) "Nested launch override | beta" (Supported table) VIABLE-WITH-CAVEAT (preview channel only on Windows) [ART Windows ships preview-channel only: preview-2026-07-29 → herdr-windows-x86_64.zip; stable v0.7.5 has no Windows asset]
slaveOftime/open-relay (oly) Rust MIT [SRC gh api repos/slaveOftime/open-relay/license → spdx_id MIT; LICENSE at repo root] 89 main HEAD 2026-07-01; pushed_at 2026-07-30 (four side branches) Yes (interprocess GenericNamespaced) Yes Named-pipe IPC + HTTP/WS + push notifications L2 No nesting evidence — see row note; what exists is a windows-latest PTY test matrix over shells, not agents VIABLE-WITH-CAVEAT (unauthenticated, no pipe ACL) [SRC tests/e2e_daemon.rs, e2e_pty.rs; .github/workflows/ci.yml matrix incl. windows-latest]
dualface/qscreen Rust MIT 6 2026-07-21 [SRC gh api pushed_at; last default-branch commit "docs(readme): add feature boundaries and quick start"] Yes [SRC crates/qscreen-daemon/Cargo.toml: "Windows平台仅需tokio的named_pipe实现"] Unverified — no CI on any platform qscn CLI + ScreenFrame/ScreenRun structured protocol + AttachMode::Bytes L2 Untested UNVALIDATED Zero issues, zero PRs, zero CI ever [ART contents/.github → 404]
artyomsv/quil Go MIT [SRC gh api repos/artyomsv/quil/license → spdx_id MIT; LICENSE at repo root] 11 2026-08-01 Yes (ConPTY) but AF_UNIX IPC, not named pipes Yes — and CI runs only there MCP (18 tools) + quild CLI + TUI L2 Mouse-mode forwarding for nested alt-screen apps [SRC internal/daemon/mousemode.go] UNVALIDATED [SRC internal/ipc/server.go:311 net.Listen("unix"); go.mod has no go-winio; ipc/ has no _windows.go split]
wezterm-mux-server (wezterm/wezterm — note wez/wezterm now 301-redirects) Rust MIT 28,101 2026-07-31 Yes Yes 15 CLI verbs (list, spawn, split-pane, send-text, get-text, kill-pane, proxy…) + versioned binary codec + tmux -CC client L2 Attach/detach first-class [DOC docs/config/lua/MuxDomain/attach.md] EXTRACTABLE [SRC ci/deploy.sh:115-124 copies wezterm-mux-server.exe into the Windows zip + Inno installer; RPM spec: "Multiplexer server (headless)"; codec/src/lib.rs varint framing "so client and server can more gracefully manage unknown enum variants"]
ao pty-host (Untrivial-ai/agent-orchestrator) Go Apache-2.0 8,742 2026-08-02 Yes Yes (the surrounding ADE is cross-platform; the pty-host package itself is spawn_windows.go-gated) 8-message binary protocol [type][BE32 len][payload] over loopback TCP L2 applyLargestLocked sizes the shared PTY to the largest attached client EXTRACTABLE [SRC backend/internal/adapters/runtime/conpty/{spawn_windows.go,host_main.go,proto.go,host.go,ptyregistry/registry.go}]
owenthereal/upterm Go Apache-2.0 1,270 2026-07-25 Yes [SRC host/internal/pty_windows.go — charmbracelet/x/conpty + Job Objects] Host yes; relay uptermd is Linux-only SSH (public-key auth: --authorized-keys, --github-user, --gitlab-user…) L1 only [SRC host/host_windows.go] deliberately ignores os.Interrupt "to prevent upterm from dying when SSH clients send Ctrl+C to child processes via ConPTY" REFUTED (fails the survive-the-ADE requirement: L1 only, no detach/reattach) [ART v0.24.0 ships upterm_windows_{386,amd64,arm64}; relay uptermd is Linux-only]

Row notes.

  • psmux[SRC src/server/mod.rs:839] control listener is TcpListener::bind(("127.0.0.1",0)) with an application-layer AUTH <key> handshake [SRC src/server/connection.rs:304-315]. Two weaknesses: the key is generated from std::collections::hash_map::RandomState over a nanosecond timestamp + PID, truncated to 64 bits [SRC src/server/mod.rs:852-860] — explicitly non-cryptographic per Rust's own docs — and the key file gets no explicit ACL, with the source comment conceding "user-only visibility on Windows comes from the profile directory ACLs" [SRC src/server/mod.rs:864]. Separately, src/cross_session_server.rs opens a second, completely unauthenticated channel, and it is worse than "input injection": :98 binds TcpListener::bind("127.0.0.1:0"), :127 accepts the first connection with no handshake at all, :131-132 pushes that socket into crate::types::PIPE_WRITERS as a tee writer — so the connecting process receives a copy of every byte the pane's ConPTY emits — and :149 writes everything it reads back into pty_writer.write_all(). That is full-duplex: output exfiltration plus input injection, unauthenticated, on a loopback port any local process can scan, gated only on the user performing a cross-session pane move. Windows-only: CI builds only *-pc-windows-msvc. On the other side of the ledger, psmux's CI runs cargo audit --deny warnings against both the workspace and the test-monitor lockfile — a supply-chain signal no other small candidate matches. Self-contradicting nesting guard: the check at :954 and :4056 reads PSMUX_ALLOW_NESTING, but the error text printed at :958/:4060 says "psmux: sessions should be nested with care, unset PSMUX_SESSION to force" — a scripted wrapper that follows the message will not clear the guard.

  • rmux — the best-designed transport in the census and the only identity-scoped namespace (2.3). Anti-squatting is not unique to qscreen: rmux also uses FILE_FLAG_FIRST_PIPE_INSTANCE [SRC crates/rmux-pty/src/backend/windows/io.rs], and validates the peer with ImpersonateNamedPipeClient [SRC crates/rmux-ipc/src/stream_windows.rs], and derives the pipe name from SID + integrity level so a name cannot be guessed into. Its exposure is not technical: 936 of ~1000 commits are from a single author [UNV — commit-attribution count carried from an earlier pass, not re-verified with gh api], and the dual licence pair is unstated in metadata. Nesting behaviour was never tested.

  • wezterm-mux-server — headless by design and the only candidate whose wire format was explicitly built for version-skewed client/server pairs, which is precisely the ADE-churn insulation being shopped for. The contract-stability caveat is sharper than "ships from nightlies": the newest tagged release is 20240203-110809-5046fc22, published 2024-02-03 — two and a half years old [SRC gh api repos/wezterm/wezterm/releases]. The rolling nightly tag (which ships WezTerm-windows-nightly.zip) is the only current Windows path, so "pin a version" and "run on Windows" are in direct tension. Also note the repo moved: wez/wezterm 301-redirects to wezterm/wezterm.

  • upterm — reclassified REFUTED in this edition. It has genuinely good properties (real ConPTY via charmbracelet/x/conpty, Job Objects, the only public-key auth model in the census, Windows release artifacts) and its os.Interrupt handling is a real nested-ConPTY lesson worth stealing. But it is L1 only: the session lives exactly as long as the upterm host process. It fails the one requirement that defines this document's subject — surviving the process that spawned it — so it cannot be a middle layer, whatever else it is good at.

  • quil[SRC PR #51] documents a dated production incident (2026-06-10) where a second quild against the same QUIL_HOME unlinked the live daemon's socket and overwrote its PID file, "bricking the original for new clients" — in exactly the instance-isolation mechanism previously reported as a strength. The mechanism is one line: [SRC internal/ipc/server.go:309] os.Remove(s.path) // Clean up stale socket, executed unconditionally at the top of Start(), before net.Listen. There is no liveness check on the existing socket, so a second daemon deletes a live one's endpoint by design. Same audit fixed a VT-emulator-disposal race leaking a goroutine plus a 10,000-line scrollback grid per closed pane, an unbounded PTY-coalescer debounce buffer, and a Windows WaitExit handle leak per destroyed pane. [ART .github/workflows/*.yml] CI is ubuntu-latest only — 25+ Windows-specific source files, zero Windows CI, zero external issues ever filed.

  • oly — two things, one corrected and one new.

    • Correction: oly has no nesting evidence, and earlier editions of this document said it had the best in the field. That claim was wrong. [SRC tests/e2e_pty.rs, 895 lines] contains zero occurrences of copilot or opencode; it spawns shellscmd.exe, bash --noprofile --norc, pwsh — on windows-latest. The tests/output-copilot.log file is a 17,718-byte recorded ANSI dump consumed by [SRC src/session/logs.rs], the log-rendering unit tests — not by the PTY e2e tests, and not by anything that attaches to a live agent. What oly actually has, which is still more than any other small candidate, is a real windows-latest PTY test matrix. It tests shells under ConPTY on Windows; it does not test nesting and it does not test agent CLIs.
    • New finding from the same fixtures, and it cuts against constraint 3: oly ships tests/output-copilot.expected.windows alongside tests/output-copilot.expected — a Windows-specific expected output for the same input log. That is direct evidence that oly's VT/log-rendering layer produces different output on Windows than on Unix. For a document whose constraint 3 values uniform Windows+Linux behaviour, that is a more interesting fact than the live-agent-tests claim it was mistaken for.
    • Security, unchanged and still disqualifying-adjacent: [DOC milestones/M4_security_hardening.md, status "⏳ Not started" since 2026-03-12] lists "Windows: enforce named-pipe ACL (current-user SID only)" as unimplemented. [SRC src/ipc.rs, 296 lines] contains no ACL/SID/security-descriptor keyword, and FILE_FLAG_FIRST_PIPE_INSTANCE, security_descriptor and SECURITY_ATTRIBUTES are all zero hits in the repo. [DOC learn.microsoft.com/windows/win32/api/winbase/nf-winbase-createnamedpipea] a NULL security descriptor grants "read access to members of the Everyone group and the anonymous account" — for a pipe carrying agent terminal I/O, that is an API-key disclosure path. Its later security audit [SRC PR #83] fixed the web layer (3 High: command injection in notification hooks, permissive CORS, no rate limiting on node-join; 14 Medium; 4 Low) and Unix socket modes, but no Windows pipe-ACL item appears in the findings table.
  • qscreen — does two things oly does not: .first_pipe_instance(true) on the initial listener (anti-squatting) [SRC crates/qscreen-daemon/src/lib.rs:162] and per-user pipe namespacing. It is not unique in this — rmux does both as well (see the rmux row note); the contrast is with oly specifically, which does neither. Its VT crate crates/vt100-psmux is a cargo vendor of the published vt100-psmux 0.16.2 (Jesse Luehrs' vt100-rust, patched, republished by marlocarlo) — the same crate psmux vendors, independently, not a fork of psmux's directory. Its portable-pty is 0.8, a full minor behind oly's 0.9 and psmux's patched 0.9.6, in a crate whose Windows backend changes between minors.

  • zellij[SRC zellij-client/src/os_input_output_windows.rs:32] chooses between a VT-byte input path and a native-console INPUT_RECORD path via use_vt_path() { env::var("TERM").is_ok() || env::var("WT_SESSION").is_ok() }. Inside ORCA's node-pty ConPTY neither is necessarily set. Judgement, downgraded from "hazard" to "config check": the else branch is the native-console INPUT_RECORD path, which is implemented, is the conservative choice under a real console, and ConPTY does give the child a real console. So the honest statement is "Zellij picks the native-console path, untested in this topology, and one env var (TERM=xterm-256color) forces the other". Calling it the wrong path was unevidenced. What is genuinely unknown is the observable symptom if the native-console path misbehaves under a nested ConPTY — dead keyboard, garbled input, or silently fine. Q3 has no pass/fail criterion until someone runs it; that limitation is recorded in section 9. Known Windows rough edges from maintainer divens' tracking issue #4745: no sixel, unreliable OSC 7 / live-cwd under PowerShell, focus-report sequences not reaching subpanes, scroll-mode stickiness. Two citation-path corrections from the audit: use_vt_path() is at os_input_output_windows.rs:32-33, not :91-99; and default.kdl is at zellij-utils/assets/config/default.kdl — the bare assets/config/default.kdl path cited in earlier editions 404s. There is also a second, 100 ms poller at :38/:52 alongside the 50 ms resize poll documented in 4.1.

  • herdrthe Windows-beta page must be read as two tables, and earlier editions read only one. Its Supported table lists "Local persistent sessions | beta", "Native panes through ConPTY | beta", "Windows Terminal / PowerShell app attach | beta", "Pane screen history | beta" and "Nested launch override | beta". Its Not supported table lists "Direct terminal attach | unsupported", "Live server handoff | unsupported", "Unix file-descriptor handoff | unsupported", "Unix foreground process groups | unsupported", "Prefix input-source switching | unsupported" [DOC website/src/content/docs/windows-beta.mdx]. Judgement: read together, "Direct terminal attach" sits in a cluster of Unix-specific mechanisms (fd handoff, live server handoff, foreground process groups) and is most likely the fd-passing attach path, not the ADE-spawn path — which is separately listed as supported in beta. That makes Q2 much less likely to be a showstopper than earlier editions claimed, and it is why Q2 has been demoted from "highest-value single verification in this whole map". It still needs confirming, because the inference is this document's, not herdr's.

  • ao pty-host[SRC conpty/host.go] "replays scrollback to new clients, fans output to all connected clients"; "On PTY exit it broadcasts a status update but stays alive (keep-alive, mirroring tmux behavior)". [SRC conpty/ptyregistry/registry.go] persists ~/.ao/windows-pty-hosts.json with {sessionId, ptyHostPid, pipePath} so orphans are findable after metadata loss. [SRC host_main.go] carries its own caveat: "loopback bind only; any local process on this host can connect to the assigned port. A per-session random token handshake is the upgrade path." Apache-2.0 makes vendoring legally clean, unlike the AGPL candidates.

Install and prove-it commands. Section 8 proposes hands-on tests; these are the exact commands to get to a running daemon and to prove the one property that matters — that a session survives its parent. Run each from a normal PowerShell prompt. [UNV — assembled from each project's own install docs and verb list; not executed on this machine in any pass]

# psmux        — winget install psmux.psmux    (or: scoop install psmux)
psmux new-session -d -s probe ; psmux ls          # detached session, then list it
# Zellij       — winget install zellij-org.zellij   (or the v0.44.3 msvc .msi)
zellij --session probe options --help ; zellij list-sessions
# rmux         — winget install Helvesec.rmux   (or: scoop install rmux / choco install rmux)
rmux new -d -s probe ; rmux ls
# herdr        — download preview-2026-07-29 herdr-windows-x86_64.zip  (NOT stable v0.7.5 — no Windows asset)
herdr list
# wezterm-mux  — download WezTerm-windows-nightly.zip  (no current tagged release; see row note)
wezterm-mux-server --daemonize ; wezterm cli list
# OpenCode     — winget install anomalyco.opencode
opencode serve --port 4096                        # then: POST /pty, GET /pty/:id/connect (WS)

# The one command that proves detachment, for any of the above:
#   1. start a session,  2. kill the parent terminal window outright,
#   3. open a new terminal and run the tool's `ls`/`list` verb.
#   If the session is still listed and re-attachable, it is L2. If not, it is L1.

3.2 Session daemons — POSIX-only ancestors and refuted candidates

Name Runtime License Stars Last activity Native Win Persist Verdict Evidence
GNU screen C GPL last release 4.9.1 (2023) No L2 CONTEXT (ancestor) [SRC github.com/Orc/screen README] — Laumann's original beta-test README, preserved as an artifact: "Since 'screen' uses pseudo-ttys, the select system call, and UNIX-domain sockets, it will not run under a system that does not include these features of 4.2 and 4.3 BSD UNIX." Also: full VT100/ANSI-X3.64 emulation and the ^A prefix are in the original. Provenance correction: the file carries no date, and the distribution channel it names is mod.sources, not net.sources. Earlier editions dated it "1987 net.sources"; neither element is supported by the artifact.
tmux C ISC 3.7b (2026-07-01) No L2 CONTEXT (ancestor) [2ND undeadly.org/cgi?action=article;sid=20090712190402] Marriott: screen had "poor documentation, a strange configuration file and an unintuitive command-line interface… And that isn't mentioning the code". [SRC cmd-attach-session.c:74-76] nested-session refusal string. (Tagged [2ND]: an interview on a third-party news site, not tmux's own documentation.)
dtach C public-domain-ish 732 2025-06-20 No L2 REFUTED (POSIX) [SRC README] "assumes that the host system uses POSIX termios, and has a working forkpty function available"
abduco + dvtm C ISC 969 / 956 2020-04-30 / 2021-03-06 No L2 / — REFUTED (POSIX + dormant) [DOC brain-dump.org/blog/abduco-dvtm-a-lightweight-alternative-to-tmux-and-screen/] — note this is the article URL; the bare brain-dump.org/blog index cited in earlier editions does not contain the quote
coder/boo Zig MIT 748 2026 (bare year — pushed_at not pulled this pass) [UNV] No L2 REFUTED (POSIX) [DOC coder/boo README] "your terminal <-(raw tty)-> boo client <-(unix socket)-> session daemon"; [SRC std.posix in 11 files, AF_UNIX 0 hits]. Notable for swapping screen's ancient emulator for libghostty-vt and shipping peek --json/wait --idle as first-class.
Roost Swift+AppKit / Rust+gtk4 unstated 2026 (bare year — pushed_at not pulled this pass) [UNV] No L1 REFUTED [DOC charliek.github.io/roost] "Not a Windows app — macOS and Linux only."
Gaurav-Gosain/tuios Go No REFUTED Unix-socket-only daemon, zero Windows build tags, procfs dependency
fukuyori/wtmux No REFUTED Detach/attach documented as "planned"
weill-labs/amux darwin+linux releases only REFUTED Release artifacts
prompt-toolkit/pymux Python 2023 REFUTED Unmaintained
chyyran/conmux 2019 No REFUTED Abandoned, no session support
mobydeck/atch, yazgoo/diss, gHexaByte/retach, whisperity/monomux No REFUTED POSIX sockets
densharik/kip REFUTED In-process TUI, not a daemon
smtg-ai/claude-squad Go AGPL-3.0 8,222 2026-07-30 No REFUTED [SRC go.mod] creack/pty v1.1.24 and no ConPTY library of any kind, despite active maintenance; hard tmux prerequisite; open issue #275
devflowinc/uzi Go MIT 580 2025-06-04 Unknown UNVALIDATED (dormant) Hard tmux prerequisite; zero Windows issues ever filed — neglect, not confirmed non-support
nelhage/reptyr C MIT 6,309 2025-11-20 [SRC gh api pushed_at] — 8+ months dormant, not "active" as earlier editions said No CONTEXT The POSIX solution to the structurally identical problem (grab I/O of a process you didn't spawn) via ptrace. No Windows analogue exists. 10+ years, and no attempted port was located [UNV — absence of a port is an absence-of-evidence finding from repo and issue searches, not an exhaustive proof] — read as evidence the platform makes this hard, not that the niche is unfilled.

3.3 PTY-as-a-service — terminal over HTTP/WS/SSH

Name Runtime License Stars Last activity Native Win Interface Persist Verdict Evidence
OpenCode /pty API TS/Bun MIT 192,203 2026-08-02 Yes GET/POST /pty, GET|PUT|DELETE /pty/:id, POST /pty/:id/connect-token, GET /pty/:id/connect (WS) L1+ replay (see 2.9) VIABLE-WITH-CAVEAT (undocumented contract; UTF-8 string wire, not raw bytes) [SRC packages/opencode/src/server/routes/instance/httpapi/groups/pty.ts; packages/core/src/pty/protocol.ts — "raw UTF-8 terminal chunks… one control frame — a 0x00 byte followed by UTF-8 JSON — carries the absolute output cursor after replay so clients can resume later"; pty.node.ts: process.platform === "win32" ? { useConptyDll: true } : {}]. Documented nowhere on opencode.ai — the published server doc has zero /pty occurrences. Byte-fidelity caveat, from the same 38-line file: chunks(data: string) slices at 64 KiB in UTF-16 code units (surrogate-splitting hazard), and decodeInput drops invalid UTF-8 input silently via new TextDecoder("utf-8", { fatal: true }) inside a bare catch. See 2.2.
ttyd (tsl0922) C MIT 12,113 code 2026-03-20 Source yes, shipped binary broken HTTP + raw WS L0 REFUTED as shipped [SRC src/protocol.c:154,353,377-383] a new process per WebSocket, unconditionally killed on close. [DOC issue #1501] the only Windows release (ttyd.win32.exe, MinGW, 1.7.7, 2024-03-30) fails to spawn any child on Windows 11 build 26200 — your exact build. MSVC fix merged 2026-03-19; no release cut since. WinGet and Scoop both pin the broken 1.7.7.
gotty (yudai / sorenisanerd) Go MIT 19,531 / 2,523 2024-08-01 / 2026-08-01 No HTTP + WS L0 REFUTED [SRC creack/pty start_windows.go: return nil, ErrUnsupported]. The active fork's release matrix ships freebsd/netbsd/openbsd/solaris and deliberately omits windows.
wetty (butlerx) Node MIT 5,362 2026-07-31 No HTTP + socket.io L0 REFUTED [SRC src/server/spawn.ts] every PTY, including --command, is pty.spawn('/usr/bin/env', cmd, xterm); src/server/spawn/env.ts parses GNU coreutils version strings; src/server/command.ts gates local mode on process.getuid?.() === 0, undefined on Windows
terminado / jupyter_server_terminals Python BSD-3 373 / 20 2025-08-02 (docs only; last code 2024-04-30) / 2026-01-14 Yes (pywinpty) REST + WS ["stdout",text] etc. L1 (NamedTermManager), L0 (UniqueTermManager) CONTEXT (design template) [SRC terminado/management.py:23-34] — a nested try/except ImportError chain, not a platform conditional; if both imports fail it binds PtyProcessUnicode = object and fails later with an obscure AttributeError. [SRC pywinpty winpty/ptyprocess.py] bridges non-selectable ConPTY output by binding a per-PTY ephemeral 127.0.0.1 TCP listener pumped from a daemon thread — one loopback port and one polling thread per terminal, an accept() race any local process can win, output round-tripped through str, and a literal '0011Ignore' sentinel injected on empty reads
ekzhang/sshx Rust MIT 7,567 2025-06-19 (13.5 mo) Yes (zhiburt conpty, not portable-pty) gRPC client↔relay + custom canvas UI L1 REFUTED for this use [SRC crates/sshx/src/terminal/windows.rs] author's own comment: "I can't get powershell.exe to work with ConPTY, since it returns error 8009001d" — probe list is Git Bash then cmd.exe. No arbitrary-command arg (only --shell), no detach, [DOC ekzhang/sshx README] "Self-hosted deployments are not supported at the moment", [ART gh api repos/ekzhang/sshx/releases — every entry has an empty assets array] every release has zero assets
tmate C (tmux fork) BSD 6,092 2026-07-29 No SSH L2 REFUTED (tmux fork — inherits tmux's POSIX dependency) Project is alive. [UNV] The "shutting down" claim is attributed to a downstream project's README rather than tmate itself; that attribution was not re-verified this pass — treat "a downstream README, not tmate" as unconfirmed, and the pushed_at date as the actual evidence of liveness. Free relay has an open reliability issue (#322, stale since 2025-11)
Coder agent/reconnectingpty Go AGPL-3.0 (+ proprietary enterprise/) 14,004 2026-08-02 Yes Coordinator mesh → WS/SSH L1, self-terminating PRIOR-ART ONLY [SRC agent/reconnectingpty/reconnectingpty.go] two backends; screen.go gated runtime.GOOS == "linux" ("Screen seems flaky on Darwin"); buffered.go is a 64 KiB ring dumped raw [SRC buffered.go:52, 222-223], self-labelled "(buggy)". Closes itself after Options.Timeout (default 5 min); 30 s initial attach window. Called from exactly one non-test place — needs a coderd control plane + Postgres.
VS Code ptyHost (microsoft/vscode) TS MIT active Yes VS Code internal IPC L1 across window reloads PRIOR-ART, strongest [SRC src/vs/platform/terminal/node/ptyService.ts] PersistentTerminalProcess with detachFromProcess(id, forcePersist) (:403), shouldPersistTerminal (:416), orphan-question protocol, serializeTerminalState(ids) (:230). Replay is not a raw byte dump: it runs @xterm/headless (:33,39,1033) + @xterm/addon-serialize (:1133) to serialize real terminal state, and terminalRecorder.ts records resize events too, MaxRecorderDataSize = 10 MB. Contrast Coder: 10 MB state-aware + resize replay vs 64 KiB raw conn.Write(prevBuf). Open-source server packaging (openvscode-server) is Linux-only.
andyk/ht Rust Apache-2.0 902 2025-07-25 No HTTP + WS + NDJSON stdio REFUTED (POSIX) Description is nearly your brief verbatim: "wrap any binary with a terminal interface for easy programmatic access". Embeds avt so callers query the rendered screen, not the byte stream. [SRC src/pty.rs] forkpty + nix + tokio::io::unix::AsyncFd. The shape to copy, not the tool to use.
PowerShell/Win32-OpenSSH C BSD-style 8,223 2025-10-27 Yes SSH L0 CONTEXT First-party, ConPTY-backed, installable as a Windows optional feature. Zero session persistence — a transport, not a middle layer. [DOC issue #2291, filed by MSFT member SteveL-MSFT, open]: "On Linux, you can use something like tmux to keep a long lived session, but the equivalent doesn't exist on Windows."
wavetermdev/waveterm Go+TS Apache-2.0 21,892 2026-07-31 Yes GUI terminal CONTEXT Gets ConPTY out of a Go stack via replace github.com/creack/pty => github.com/photostorm/pty — a 15-star fork last pushed 2024-04-14. The practical escape hatch for Go projects blocked by creack/pty's stub, and a named supply-chain risk.
code-server, Gitpod, Eclipse Che, Butterfly, shellinabox, tty-share, webssh, Guacamole, Teleport various various No (or wrong category) REFUTED / CONTEXT code-server [DOC docs/install.md] "We currently do not publish Windows releases"; Gitpod/Che are Linux-container-only; Butterfly [SRC setup.py classifiers] POSIX :: Linux, last commit 2018; shellinabox last commit 2019; tty-share [SRC go.mod] creack/pty 1.1.11; webssh and Guacamole are relays to an existing endpoint, never own a local PTY; Teleport [DOC goteleport.com/docs — installation/supported platforms] "Teleport server does not run on Windows yet"
Eclipse Theia TS EPL-2.0 21,617 2026-08-01 Partial HTTP/WS or Electron IPC attach-by-id exists UNVALIDATED (server-mode packaging) [SRC packages/terminal/src/node/shell-process.ts:90-91] if (isWindows) { return 'cmd.exe'; }; [SRC base-terminal-server.ts:49] async attach(id: number) with a backend terminal registry

3.4 ADEs and agent hosts

Why an ADE census is in a document about the layer underneath ADEs. Two reasons, both load-bearing. First, it is the evidence base for the churn argument that motivates the whole project: six of the entries below died, sunset or closed-sourced within eight months, which is what makes ADE-independence worth paying for. Second, the middle layer can only be inserted where an ADE lets you override the launch command — so this table is the proof that such hooks exist in the wild, and the sample of what shape they take.

The decisive filter: genuine free-form command override. Among the native-Windows candidates, only four document a real "point me at any executable" hook a third party can use without forking source: ORCA, kandev, Pane and Zed (ACP-shaped). Three further candidates document an override at least as good but are refuted on platform, not on override — cmux (cmux.json, cmux hooks setup --agent; macOS only), superset (the best-documented override of any candidate; "Windows/Linux untested"), and sculptor ("run and manage any terminal-based agents"; Mac/Linux downloads only). Counting all seven, override hooks are not rare; native-Windows override hooks are.

Name Runtime License Stars Last activity Native Win Override Own control surface Verdict
ORCA (stablyai) Electron/TS, node-pty MIT 35,379 2026-08-02 Yes YesagentCmdOverrides [SRC src/shared/types.ts; consumed src/shared/tui-agent-launch-command.ts:30] CLI verbs + MCP registry Your ADE
kandev (kdlbs) Go + web AGPL-3.0 528 2026-08-02 (v0.83.0) Yes [SRC apps/backend/go.mod: UserExistsError/conpty v0.1.4 + build-tag-split PTY files + cmd/winjob/] Yes — Settings → Agents → Add TUI Agent (parsed with strings.Fields) [DOC docs/public/agents-and-profiles.md] Extensive: ws://localhost:38429/ws [DOC websocket-api.md], four MCP modes incl. External MCP [DOC automation-and-mcp.md], agentctl HTTP route groups, plugin marketplace, service install VIABLE
Pane (dcouple) Electron + Node/Python AGPL-3.0 + attribution 348 2026-07-31 Yes (@lydell/node-pty prebuilds) [DOC docs/BUILDING_ON_WINDOWS.md] Yes--tool-command <command> [DOC docs/RUNPANE_CLI_CONTRACT.md] runpane CLI (all --json) + machine-readable contracts/runpane/contract.json + first-party Python client + remote daemon VIABLE
Zed Rust/GPUI GPL-3.0 core + Apache-2.0 crates 87,888 v1.13.1 2026-07-29 Yes Yes, ACP-shaped [UNV — not re-verified in any pass; see section 9]agent_servers in settings.json; spawned process must speak ACP JSON-RPC, not a raw PTY ACP VIABLE (different shape)
Agent Orchestrator (Untrivial-ai) Go + Electron Apache-2.0 8,742 2026-08-02 Yes No — 30 compiled Go packages under backend/internal/adapters/agent/ ao CLI over a loopback HTTP REST API [DOC docs/cli/README.md]: ao spawnPOST /api/v1/sessions, ao send, ao session ls|get|kill|restore, ao agent ls, ao status --json, ao doctor --json, ao completion powershell; AO_SESSION_ID/AO_PROJECT_ID let a nested agent resolve itself VIABLE — contains the middle layer
emdash (generalaction) Electron Apache-2.0 5,320 2026-08-02 Yes (.msi + .exe) No — fixed 35-provider enum [DOC agents/integrations/providers.md] None found CONTEXT
paseo (getpaseo) Electron + Node daemon + Expo AGPL-3.0 11,862 v0.2.5 2026-07-30 Yes No — fixed enum [SRC packages/protocol/src/provider-manifest.ts] WebSocket API + MCP server in packages/server CONTEXT
Nimbalyst Electron MIT 1,373 2026-08-01 Yes No — exactly 4 agents; its extension system is for file-type editors, not agents [DOC Nimbalyst README] Desktop + mobile CONTEXT
Vibe Kanban (BloopAI) Rust + npx Apache-2.0 27,622 SUNSET 2026-04-24 No MCP + HTTP + relay tunnel DEAD[SRC gh api repos/BloopAI/vibe-kanban/commits] second-to-last commit is literally "Add README sunsetting banner"; README <h1>: "Vibe Kanban is sunsetting." 534 open issues, repo left read-write
Crystal (stravu) MIT 3,107 DEPRECATED Feb 2026 DEAD — produced two descendants, not one: Nimbalyst (named successor) and Pane [SRC dcouple/Pane docs/CRYSTAL_ARCHITECTURE.md, a renamed Crystal doc]; Nimbalyst still ships crystal-run.ps1
Terragon Apache-2.0 253 SHUT DOWN Jan/Feb 2026 DEAD — OSS snapshot only
Roo Code (RooCodeInc) TS Apache-2.0 24,361 ARCHIVED, last push 2026-05-15 Yes VS Code ext DEAD, no stated reason
cmux (manaflow-ai) Swift+AppKit GPL-3.0-or-later (GitHub reports NOASSERTION only because of a dual-offer preamble) 25,483 2026-08-02 No — macOS only Yes (cmux.json, cmux hooks setup --agent) CLI hooks REFUTED — but holds the only level-3 persistence claim in the census
superset-sh/superset Bun NOASSERTION 12,733 2026-08-02 No ("Windows/Linux untested") Best documented override of any candidate — Settings → Agents, "add any terminal agent" REFUTED (platform)
imbue-ai/sculptor desktop MIT 212 2026-08-01 No (Mac/Linux/Linux-ARM64 downloads only) "run and manage any terminal-based agents" REFUTED (platform)
coder/mux Electron AGPL-3.0 1,950 2026-08-02 Yes (v0.28.1 ships .exe despite README saying otherwise) N/A — has its own agent loop, does not spawn external CLIs VS Code ext REFUTED (wrong architecture)
OpenChamber Electron/web MIT 7,122 2026-08-02 Yes No — bundles the matching OpenCode CLI --lan/--host, SSH remote CONTEXT
automagik-dev/genie; iishyfishyy/operator-oss TS MIT / Apache-2.0 327 / 170 2026-07-30 / 2026-08-01 Unknown Unknown Unknown UNVALIDATED
Long tail (topic sweep) mixed mixed 0-350 each mostly 2026-07/08 Unknown UNVALIDATED — wibus-wee/cradle-app (50★), kaanozhan/Frame (319★), soromi (32★), uxnan (24★), Termote (28★), workstream-labs/workstreams (76★, stale 2026-04-22), Zeus-Deus/codemux, construct-worlds/construct, ~24 more <30★
GitKraken Kepler; JetBrains Air; Warp; Factory/Droid; Devin; Google Jules; Cursor; Conductor; AIDEN closed proprietary Yes (except Conductor: Mac-only) No documented arbitrary-CLI override found in any [UNV — closed-source; nothing here was verified at source in any pass] Warp: Oz CLI + HTTP API + Python/TS SDKs; Air: MCP + provider accounts [UNV] CONTEXT (commercial)
Cline, Kilo Code, opencode, Amp, Codebuff Apache-2.0 / MIT / MIT / proprietary / Apache-2.0 65k / 26.7k / 192k / — / 8.2k all 2026-08 (month granularity — exact pushed_at not pulled) [UNV] CONTEXT — these are the agents, i.e. the thing a middle layer wraps [UNV — the agent-vs-host classification was not re-verified; see section 9]. Note sst/opencode now redirects to anomalyco/opencode; opencode-ai/opencode is a different, archived project (13,604★, last push 2025-09-18) — do not conflate
microsoft/intelligent-terminal MIT 1,639 2026-08-02 Yes (Win 10 19041+) Auto-detects Copilot/Claude/Codex/Gemini/OpenCode ACP over stdio CONTEXT — a Windows Terminal fork with an agent pane; itself an ADE-layer competitor
batrachianai/toad Python/Textual AGPL-3.0 + commercial-license topic 3,350 2026-05-26 No ACP client REFUTED — [DOC batrachianai/toad README:175-177] "Toad runs on Linux and macOS. Native Windows support is currently lacking… but Toad will run quite well with WSL"; roadmap - [ ] Windows native support unchecked

Agent-churn casualty count: 6. Crystal (Feb 2026), Terragon (Jan/Feb 2026), Roo-Code (archived, 2026-05-15), Vibe Kanban (sunset 2026-04-24, 27.6k stars — the largest), DevPod (abandoned, see 3.5), Daytona (closed-sourced June 2026). Method note: "stale" and "sunset" were conflated once in this research. Any candidate whose last push is >60 days old needs a specific check for a shutdown notice, not just a date.

3.5 Containers, sandboxes, isolation

Read this first — it is why the twenty rows below do not change the answer. Containerising an agent session relocates the PTY-ownership problem; it does not eliminate it. Every viable container path still needs something inside the boundary to own the session — tmux-in-pod, or sbx's proprietary attach layer — and adds one more VT-parsing and resize boundary on top of ORCA's own PTY. Containers are an optional outer isolation shell around the middle layer, not a replacement for it. The census is here because the WSL question had to be settled precisely rather than waved at, and because two open-source non-WSL paths do survive.

Name Category License Native Win without WSL Persist+reattach Verdict Evidence
Windows Containers (process isolation) OS feature N/A Yes REFUTED [DOC learn.microsoft.com] Windows kernel only; cannot run Linux binaries
Windows Containers (Hyper-V isolation) OS feature N/A Yes REFUTED Real VM boundary, but the guest is still Windows. Page "Applies to" lists Server 2016-2025 only, no Windows 11 client SKU [UNV — the "Applies to" field was quoted from the docs page; no URL was recorded and it was not re-retrieved]
Docker Desktop, Hyper-V backend container runtime proprietary, paid above 250 employees / $10M Yes (all-users install, Pro/Ent/Edu, com.docker.service) REFUTED on constraint 2 [DOC docs.docker.com/desktop/setup/install/windows-install/] "Docker Desktop's functionality remains consistent on both WSL and Hyper-V, without a preference for either architecture" — the widely-repeated "Hyper-V is deprecated" blog claim is false. Same page: docker-users membership "is equivalent to granting administrative privileges on the host"
Podman, hyperv machine provider container runtime Apache-2.0 Yes VIABLE (one of two) [DOC containers/podman docs/tutorials/podman-for-windows.md] "Because Podman uses WSLv2 or Hyper-V recent features, you need Windows 11 or later"; Hyper-V Administrators group required (mitigable once via podman system hyperv-prep); "WSL and Hyper-V machines cannot run simultaneously". Scriptable: podman machine init --provider hyperv, CONTAINERS_MACHINE_PROVIDER=hyperv, [machine] provider = "hyperv"
minikube --driver=hyperv k8s-for-one Apache-2.0 Yes VIABLE (two of two) [DOC minikube.sigs.k8s.io/docs/drivers/ (page footer: "Last modified September 25, 2025")] Windows drivers list "Hyper-V — VM (preferred)" — first-class, not a fallback. Scope correction: the same list marks "Docker — VM + Container (preferred)" as well. Both carry the label; quoting only Hyper-V overstates the point. What survives is that Hyper-V is not a fallback
Docker Sandboxes (sbx) agent microVM closed (docker/sbx-releases is binaries-only) Yes, Windows Hypervisor Platform, no edition gate, "Docker Desktop is not required" [DOC docs.docker.com/ai/sandboxes/get-started/] Yessbx create --name X claude . then sbx run --name X "re-attaches from anywhere"; "Sandboxes persist after the agent exits"; active-session concept; SSH; documented CI/headless path [DOC docs.docker.com/ai/sandboxes/usage/] REFUTED on constraint 2 only Two pages, cited separately above — the prerequisites live on get-started/, the session semantics on usage/. Judgement: architecturally the closest existing thing to what you want. Requires sbx login (Docker OAuth, browser). No scrollback-replay claim.
Rancher Desktop k8s-for-one Apache-2.0 No REFUTED [DOC docs.rancherdesktop.io — installation prerequisites] "Rancher Desktop requires Windows Subsystem for Linux on Windows" — unconditional
k3s k8s distro No REFUTED [DOC docs.k3s.io/installation/requirements] Linux-only by its own requirements page — no Windows server node support
kind k8s-for-one Apache-2.0 Inherits backend VIABLE-by-inheritance Backend-agnostic auto-detect. Note: Docker Desktop's "bundled Kubernetes" is kind (or kubeadm) — they are not two candidates [DOC docs.docker.com/desktop/features/kubernetes/]
dagger/container-use agent MCP Apache-2.0 Partial and broken REFUTED (maturity + Windows) [DOC PR #252, open since 2025-07-24] "#242 added Windows compilation but no installation method". [SRC cmd/container-use/terminal_windows.go] discards caller args (cmd.Args = append([]string{"dagger","run"}, os.Args...)) — open fix PR #348. Last release 2025-08-19; only 2026 commits are two docs edits. Registers as container-use stdio — a stdio MCP server, which cannot own persistent sessions
loft-sh/DevPod devcontainer client OSS Docs only show WSL REFUTED (abandoned) Last release v0.7.0-alpha.34 (2025-06-23); [DOC issue #1946] "their attention is focused on vcluster… they do not have plans at this moment to allocate resources to maintaining this project"
daytonaio/daytona agent sandboxes none — license removed REFUTED (closed-sourced) Repo root now contains only README.md and assets; GET /license → 404. README: "As of June 2026, Daytona's core development has moved to a private codebase." 72k stars on an empty repo
anthropic-experimental/sandbox-runtime (srt) OS sandbox Apache-2.0 Yes, alpha — dedicated srt-sandbox local user + Windows Filtering Platform egress fence keyed to that SID N/A CONTEXT — and architecturally hostile The cross-track finding: [DOC README] the sandboxed process is launched via CreateProcessWithLogonW under a different local user account, "not as the calling user". Two consequences: (a) [INF] a console owned by a foreign user SID is not AttachConsole-able by your process; (b) [DOC README, no inference needed] "Per-user tool installs are not reachable… tools installed under your profile (nvm/fnm-managed Node, per-user winget/Scoop packages, pip install --user, %LOCALAPPDATA%\Programs\…) resolve on the inherited PATH but cannot be opened by the sandbox account." Claude Code's default Windows install lands exactly there. 4,824 stars, pushed 2026-07-24
Claude Code built-in Bash sandbox agent sandbox No REFUTED [DOC code.claude.com/docs/en/sandbox-environments] verbatim: "This option does not support native Windows. On Windows hosts, use WSL2 or one of the container or VM approaches below."
Windows Sandbox OS feature N/A Yes No — resets on close, no CLI attach model REFUTED Fails headless-first and persistence
LCOW (Linux Containers on Windows) historical DEAD [DOC docs.docker.com/engine/deprecated/] deprecated v20.10, removed v23.0: "the feature never reached completeness… Developers who want to run Linux workloads on a Windows host are encouraged to use Docker Desktop with WSL2 instead"
docker attach / exec attach semantics L1-ish CONTEXT attach shows only PID-1's stream, no scrollback replay; CTRL-p CTRL-q detach requires -i -t. [DOC docs.docker.com/reference/cli/docker/container/attach/] --detach-keys is remappable, per-container or globally — the prefix collision is configurable away, exactly like tmux's prefix. This is the only documented prefix-remapping mechanism found in the whole census; see 4.3. moby/moby#2838 and #2855 (both 2013) still cited
kubectl attach / exec attach semantics L0 CONTEXT Fresh process every time. Community's own fix is tmux inside the podpredatorray/kubectl-tmux-exec, which is 100% Shell+Makefile [UNV — language breakdown taken from repo metadata, not re-pulled this pass] and needs bash+tmux on the client, so it cannot run on native Windows
OpenHands runtime agent fleet No REFUTED [DOC docs.all-hands.dev — installation/local-setup] "OpenHands only supports Windows via WSL… Native Windows is not officially supported" (supersedes an older non-WSL guide)

The container/WSL tension, resolved precisely. It is narrower than "containers on Windows = WSL only", but the surviving set is small. Hyper-V-the-hypervisor and WSL2-the-compatibility-layer are different code paths over the same primitives; the non-WSL path exists but is consistently the less-traveled option: all-users installs, admin at install time, and a Windows Pro/Enterprise/Education edition gate (Home cannot run Hyper-V). After applying constraint 2, exactly two open-source non-WSL container paths survive on native Windows: Podman with --provider hyperv, and minikube --driver=hyperv. Everything else is closed (Docker Desktop, sbx, Daytona), hard-WSL (Rancher Desktop, k3s, Claude Code's sandbox, OpenHands), dead (DevPod, container-use), or not a Windows product (Sculptor).

And the thesis at the top of this section survives the whole census intact: the two viable paths still need something inside the boundary to own the session, so they are an outer shell around the middle layer, never a substitute for it.

3.6 Protocols and wire formats

Two kinds of row, in one table, distinguished by the Kind column. Contract rows are things you could adopt — a wire format with an implementation you can speak to today. Precedent rows are things you could learn from — designs from other ecosystems that solved a structurally identical problem, with no code you would ship. Do not read a CONTEXT verdict on a Precedent row as a weaker version of a VIABLE verdict on a Contract row; they are not on the same axis.

Name Kind Shape License Adoption Native Win Persist/detach Verdict Evidence
tmux control mode (-C/-CC) Contract Line-based text: commands in, %begin/%end/%output/%window-add/%layout-change out ISC ≥5 implementers, not 1 Yes, via psmux and WezTerm Inherits tmux/psmux CONTEXT → strong reusable contract [DOC tmux.1 CONTROL MODE]. Implementers: iTerm2; WezTerm [SRC wezterm-escape-parser/src/tmux_cc/tmux.pest, mux/src/tmux.rs, tmux_pty.rs:90-91 #[cfg(windows)] fn as_raw_handle; changelog records a **Windows-specific tmux -CC bug being fixed**]; psmux [SRC src/main.rs:4194, src/control.rs 554 lines, docs/control-mode.md 432 lines, 3 PowerShell tests]; Tomiyou/ivyterm (48★); paulrobello/par-term (par-term-tmux crate)
WezTerm mux codec Contract Versioned binary, varint length/ident/serial MIT 28k-star project Yes L2 VIABLE [SRC codec/src/lib.rs] "length, ident and serial number are encoded using a variable length integer encoding… client and server can more gracefully manage unknown enum variants" — explicitly designed for version-skewed client/server pairs, i.e. the ADE-churn insulation you are shopping for
OpenCode PTY wire protocol Contract Raw UTF-8 chunks + one 0x00+JSON control frame carrying an absolute output cursor MIT 192k stars Yes replay by cursor VIABLE [SRC packages/core/src/pty/protocol.ts; pty.ts:87 attach: (id, input) => Effect, lines 53-67 cursor/replay]
ao pty-host protocol Contract [1-byte type][BE32 len][payload], 8 messages Apache-2.0 inside 8.7k ADE Yes L2 EXTRACTABLE (vendor) [SRC conpty/proto.go] MsgTerminalData 0x01 … MsgKillReq 0x08
qscreen ScreenFrame/ScreenRun Contract Structured runs with FRAME_FLAG_* + FrameColor + FrameMouseMode MIT 6 stars Yes L2 CONTEXT Same "parse once, let the last hop render" idea as tmux -CC, arrived at independently
ACP (Agent Client Protocol) Contract JSON-RPC 2.0, LSP-modeled Apache-2.0 3,840★; Zed, JetBrains, Copilot CLI, Gemini CLI, Claude Code, Neovim, Toad No Windows CI — every job in .github/workflows/ci.yml is ubuntu-latest session/load (replays history), session/resume, session/list, session/fork, session/closeconversation-state continuity, not live-process attach CONTEXT [DOC docs/protocol/v1/draft/transports.mdx] — has a Transports chapter naming Streamable HTTP as transport 2 (draft) and stating "The protocol is transport-agnostic… can be implemented over any communication channel that supports bidirectional message exchange", formally sanctioning custom transports. So ACP over a Windows named pipe is spec-conformant. v2 terminal work in flight (schema/src/v2/terminal.rs, docs/rfds/v2/terminal-output.mdx)
Codex app-server Contract "JSON-RPC lite" with Thread/Turn/Item primitives OpenAI's flagship CLI Transport yes, daemon no Threads persist VIABLE with a caveat [SRC codex-rs/uds/src/lib.rs] is cross-platform (uds_windows on Windows); [SRC app-server-transport/src/transport/mod.rs:27-32] registers remote_control, stdio, unix_socket, websocket unconditionally. --listen ws://IP:PORT is a standing listener the client does not own — but [DOC app-server/README.md] labels websocket "experimental / unsupported. Do not rely on it for production workloads", and [DOC codex-rs/app-server-daemon/README.md] "The current daemon implementation is Unix-only… does not yet support Windows lifecycle management." Ships /readyz, /healthz, backpressure returning -32001, generate-ts/generate-json-schema for a version-pinned schema. [ART rust-v0.147.0-alpha.4] ships codex-app-server-x86_64-pc-windows-msvc.exe
Warp crates/local_control Contract Versioned action catalog over authenticated loopback HTTP /v1/control + Unix-socket credential broker AGPL-3.0 (client) 63,870★ Mixed — real [target.'cfg(windows)'.dependencies] block, but the auth design leans on a kernel-reported peer-UID check, POSIX-only CONTEXT — the closest open-source analogue to tmux control mode found [SRC warpdotdev/warp, branch master, crates/local_control/src/catalog.rs:4 PROTOCOL_VERSION: u32 = 1] 84 entries carry status: Implemented (earlier editions said "~60"): pane.list/split/resize/focus/navigate/close, session.list/activate, input.insert/replace, window/tab/surface/file. No output-read, no send-keys-to-PTY, no attach — that negative is confirmed by reading the whole catalog, and it is why this is not a PTY-attach protocol. But "cloud orchestration only" was false
Jupyter terminado REST+WS Contract GET/POST /api/terminals, WS ["stdout",text]/["stdin",text]/["set_size",r,c] BSD-3 Jupyter deployments Yes L1 (NamedTermManager), with cull_inactive_timeout CONTEXT — a design template, not an interop standard See 3.3 row notes
Jupyter kernel protocol Precedent 5 ZeroMQ sockets, execute_request/reply BSD-3 the notebook ecosystem Yes Yes CONTEXT Different subsystem from Terminals. bash_kernel (729★) proves it generalizes past language interpreters — but is POSIX-only [SRC kernel.py:116,139 signal.SIGPIPE; :123 pexpect.spawn] and stale since 2025-01-05
MCP for terminals Contract (absent) NEGATIVE SPACE Zero terminal/PTY SEPs among 41 Final ones [DOC modelcontextprotocol.io/seps]; dozens of independently-named mcp-terminal-server repos with no shared schema. Scope correction: earlier editions attached the Codex-harness quote to "OpenAI tried MCP for terminals and abandoned it". The source does not say that — it never mentions terminals or PTYs. What it does say [DOC openai.com/index/unlocking-the-codex-harness] is that "maintaining MCP semantics in a way that made sense for VS Code proved difficult", i.e. OpenAI abandoned MCP as its VS Code integration protocol. That is weaker evidence, and it is only suggestive of the terminal case
DAP Precedent Seq-numbered request/response/event JSON, transport-agnostic universal Yes CONTEXT (design precedent only) No primary source found of DAP driving a general shell
xterm.js attach-addon convention Contract Raw bytes over WebSocket ttyd, code-server, VS Code Yes CONTEXT [UNV] The superlative used in earlier editions — "arguably the most widely deployed de-facto terminal wire convention in existence" — is unsourced and untested, and this document separately concedes (section 9) that the convention went unexamined. It is retained as a lead, demoted to an explicit [UNV] claim, and it is the single largest known gap in this map's protocol coverage
asciicast / ALiS Contract 4 negotiated WS sub-protocols (v1.alis binary with 5-byte magic ALiS\x01 = [0x41,0x4C,0x69,0x53,0x01] + LEB128, v2.asciicast, v3.asciicast, raw) asciinema ecosystem Producer is trivially portable relay holds full VT state for late joiners [DOC docs.asciinema.org/manual/server/streaming/] — "The server maintains comprehensive state for each active stream by running the whole stream through asciinema's own virtual terminal emulator" CONTEXT — the CLI is refuted on Windows; the ecosystem is not [DOC docs.asciinema.org/manual/server/streaming/] for the magic bytes, LEB128 framing, all four sub-protocols and the late-joiner state claim. Judgement: a native-Windows producer is small, and it gets an existing OSS relay with N-consumer fan-out and late-joiner screen sync for free. The "~50 lines" figure in earlier editions was an unsupported estimate and is withdrawn
nREPL Precedent bencode over TCP, every message has an op, describe for capability discovery, clone for sessions EPL-1.0 Clojure ecosystem; 900★, pushed 2026-07-18 Yes Yes CONTEXT — best design reference in the whole map [DOC spec.nrepl.org] "solves the 'narrow waist' problem… shares a lot with the Language Server Protocol, though it predates LSP by several years… Writing an nREPL server… can be done in a couple hundred lines." Self-describing, session-oriented, no PTY
SLIME/swank Precedent s-expressions over TCP permissive 2,067★, 2026-07-30 Yes Yes CONTEXT The cleanest one-sentence statement of your goal: the frontend is disposable, the real long-running state lives behind a socket
Distributed Erlang Precedent (epmd, hidden nodes, -remsh) BEAM distribution Apache-2.0 foundational Yes Yes CONTEXT — and a citable warning [DOC erlang.org/doc/system/distributed.html Security section] cookie authentication is not cryptographically secure and communication is cleartext by default. Do not homegrow a shared-secret scheme that is weaker than one whose own docs disclaim it. (-remsh is documented in the erl man page, not this one)
TN3270E Precedent (RFC 2355) Session-state protocol: host sends a BIND image, client ACKs enterprise mainframe Yeswc3270/ws3270/s3270, Chocolatey partial CONTEXT — under-rated [DOC x3270 Scripting wiki] the x3270 family ships a headless scriptable emulator with a three-transport control plane: -script (stdio), -scriptport (TCP, not marked POSIX-only), -socket (POSIX only), -httpd (REST over HTTP). Plus X3270COOKIEFILE security cookies (new in 4.4). [SRC GitHub code search over the x3270 source tree] 179 httpd hits, 69 scriptport, zero CreateNamedPipe — on Windows the control plane is TCP loopback + HTTP REST
Source RCON Precedent TCP auth packet then command/response open spec game servers Yes No CONTEXT A reference for a tiny "one command, one reply" framing. Correction: the CVE attribution was wrong. Earlier editions cited "Documented RCE (CVE-2021-30481)" as an RCON protocol vulnerability. [SRC NVD, CVE-2021-30481] that CVE is "Valve Steam before 2021-04-17, when a Source engine game is installed, allows remote authenticated users to execute arbitrary code because of a buffer overflow that occurs for a Steam invite after one click" — a Steam invite-handling overflow, not an RCON protocol defect. The adjacent claim, that RCON is cleartext by spec, is correct and is the one that matters for a design reference. Note: developer.valvesoftware.com sits behind an anti-scraper proof-of-work wall — the spec itself was not re-verifiable this pass
DOOR32.SYS Precedent (BBS) Hands the child an already-open Winsock socket handle number retro Yes No CONTEXT — a 20-year-old precedent for "pass a handle, don't emulate a device" Replaced the DOS-era FOSSIL/INT14h fake-serial-port convention. Directly load-bearing warning: [DOC ENiGMA½ issue #175] "socket descriptors cannot be shared" in Node.js; its own docs' workaround is bivrost!, a Rust sidecar between the Node host and the handle. ORCA is Electron/Node — the BBS world hit this exact wall and solved it with the Rust-daemon shape psmux/oly/qscreen already occupy
Evennia Portal/Server split Precedent Two co-located processes over a Twisted AMP TCP socket BSD-3 2,084★, 2026-07-25 Yes [DOC evennia.com/docs — Setup/Installation, Windows section] Yes CONTEXT — the single best transferable framing found [DOC evennia.com/docs Portal-And-Server] "you can fully reload the Server and have players still connected to the game. One [sic — upstream typo for "Once"] the server comes back up, it will re-connect to the Portal and re-sync all players as if nothing happened." (Earlier editions silently corrected the typo inside what was presented as a verbatim quotation; the [sic] restores it.) Portal owns every protocol and every persistent connection; Server owns all logic and can crash/reload freely. That is your ADE-swap requirement, named and shipped, in a text-game engine
Superlogical Precedent unpublished presumptively closed commercial waitlist only Unknown — the cited post contains no Windows content at all. (Earlier editions put "Ghostty has no official Windows GUI" in this cell; true or not, it is not supported by the source cited here and is not about Superlogical) unknown REFUTED on constraint 2 [DOC mitchellh.com/writing/superlogical, 2026-07-29, full text] "I've started a new company: Superlogical"; "Sign up for the newsletter for beta access"; "we're hiring"; "I have no intention of ever commercializing Ghostty… Ghostty itself remains a non-profit." Only the consumed components are MIT. The post contains zero architecture content — no state-sync frames, no protocol description; any such claim traces to secondary journalism

3.7 PTY libraries and ConPTY plumbing

Library Language Windows backend Notes
microsoft/node-pty Node/C++ ConPTY What ORCA uses. [SRC src/win/conpty.cc:289] the only flag site is verbatim inheritCursor ? 1/*PSEUDOCONSOLE_INHERIT_CURSOR*/ : 0. [SRC src/win/conpty.h:26-27] defines exactly two extra symbols, PSEUDOCONSOLE_RESIZE_QUIRK (2u) and PSEUDOCONSOLE_PASSTHROUGH_MODE (8u)PSEUDOCONSOLE_WIN32_INPUT_MODE is not defined in that header at all, contrary to earlier editions. The "ORCA is disadvantaged on flags" conclusion is deleted — see the note below the table; it does not follow, because current ConPTY does not read those bits. A separate, real hazard: node-pty's bundled prototype has drifted from the current ConPTY ABI — it declares ConptyClearPseudoConsole(HPCON hPC) while microsoft/terminal src/inc/conpty-static.h:44 declares ConptyClearPseudoConsole(HPCON hPC, BOOL keepCursorRow). A one-argument call against a two-argument export is an ABI hazard on the useConptyDll path. Drift runs both ways: node-pty also declares ConptyClosePseudoConsoleTimeout, which the current public header does not export
wezterm portable-pty Rust ConPTY Passes INHERIT_CURSOR | RESIZE_QUIRK | WIN32_INPUT_MODE (and has for years — the Feb-2024 commits 454ec0a7/9d834586 in wezterm/wezterm were a one-day regression and its fix, not the introduction) [UNV — the two SHAs were not re-resolved this pass; treat the commit identifiers as unconfirmed and the "has for years" characterisation as the load-bearing part]. Two of those three bits are no-ops on current ConPTY (see the note below). Loads conpty.dll by bare relative name — hijackable by a host terminal's bundled copy
portable-pty-psmux 0.9.6 Rust ConPTY Vendored in-tree at crates/portable-pty-psmux. Deliberately drops INHERIT_CURSOR — the reasoning, with the source comment, is in 4.1 item 2 (earlier editions pointed at 6.1, which does not discuss INHERIT_CURSOR)
Helvesec/rmux rmux-pty Rust ConPTY PROC_THREAD_ATTRIBUTE_PSEUDOCONSOLE, Job Objects with CREATE_BREAKAWAY_FROM_JOB, FILE_FLAG_FIRST_PIPE_INSTANCE [SRC crates/rmux-pty/src/backend/windows/io.rs], and an explicit refusal path with the verbatim error string "breakaway process creation denied; refusing to run unguarded ConPTY child" [UNV — string quoted from an earlier pass; the exact file and line were not recorded and were not recovered this pass]
charmbracelet/x/conpty Go ConPTY Used by upterm; the xpty family
aymanbagabas/go-pty v0.2.3 Go ConPTY Used by Agent Orchestrator
UserExistsError/conpty v0.1.4 Go ConPTY Used by kandev
creack/pty Go none [SRC start_windows.go] literally return nil, ErrUnsupported. Its README never mentions Windows. Scope correction — the single-signal version of this test is wrong. Earlier editions called it "the single most reliable Go-project disqualifier in this research". It is not, because a Go project can depend on creack/pty and be ConPTY-capable through a second library: quil, in this document's own inventory, has creack/pty in its go.mod and charmbracelet/x/conpty v0.2.0, and works on Windows. The correct test is the compound one this document already applies in the claude-squad row: creack/pty present and no ConPTY library of any kind [SRC smtg-ai/claude-squad go.mod: creack/pty v1.1.24, zero conpty imports]. gotty, tty-share and claude-squad fail the compound test; quil passes it
photostorm/pty Go ConPTY The fork waveterm uses to escape creack/pty. 15 stars, last push 2024-04-14 — a real supply-chain risk
pywinpty Python/Rust ConPTY + winpty fallback Windows-only — not cross-platform, so it is disqualified on constraint 3 as a component even though it works. [2ND — the maintainer's 2025 Jupyter Discourse post; named in prose in an earlier pass with no URL recorded, and not re-retrieved] >30M downloads, "maintained by a single person", features "delayed… due to this lack of resources". Its architecturally interesting part is documented once, in the terminado row of 3.3, not here — the per-PTY ephemeral 127.0.0.1 TCP listener, the accept() race, the str round-trip and the '0011Ignore' sentinel. Earlier editions carried that material in two places
@lydell/node-pty Node ConPTY Windows prebuilds; used by Pane and OpenCode's Node path
bun-pty Bun FFI portable-pty (WezTerm) Used by OpenCode's Bun path; has a x86_64-pc-windows-gnu publish job
zhiburt conpty 0.7.0 Rust ConPTY Used by sshx
rprichard/winpty C++ ScreenBuffer scraping 1,380★, MIT, last push 2024-02-19. Pre-ConPTY: ReadConsoleOutputW + WriteConsoleInputW wrapped into a reusable read+write loop exposed over named pipes. Shipped for a decade in Git for Windows, MSYS2, Cygwin, pywinpty, node-pty's winpty backend. VS Code's own workaround for the AttachConsole crash is terminal.integrated.windowsEnableConpty: false, which falls back to it. Dormant and superseded — its existence-and-supersession is the cleanest single argument for ConPTY-based designs
Microsoft.Windows.Console.ConPTY NuGet redistributable Bundled by quil (scripts/fetch-conpty.sh, SHA256-pinned), node-pty (third_party/conpty, 1.25.260303002), WezTerm, ghostel. [DOC DHowett (MEMBER), microsoft/terminal#7019 comment dated 2026-06-10 — issue state: **closed as not_planned since 2023-09-29**] Framing correction: earlier editions rendered this as "Microsoft endorses this". In context it is a refusal to backport: "I'm sorry. When we backport… The architectural shift in ConPTY as of 1.22 is too broad for us to adequately contain. Use the NuGet package; it updates way faster than Windows." The practical instruction is the same — bundle the NuGet package — but the reason is that fixes will not reach the in-box ConPTY, which is a stronger argument for bundling and a weaker one about Microsoft's blessing
uds_windows (Rust) AF_UNIX WezTerm + Codex independently
interprocess (Rust) named pipes via GenericNamespaced Zellij + herdr + oly independently
VT parsers vt100-psmux 0.16.2 (a screen model over Alacritty's vte 0.15) — psmux and qscreen vendor it independently; vt100 upstream — oly; charmbracelet/x/vt — quil; libghostty-vt (Zig) — boo, Roost, ghostel; asciinema/avt (Apache-2.0, 229★) — [SRC avt Cargo.toml] its only dependencies are rgb and unicode-width, with no libc and no platform-conditional code; [INF] it should therefore compile on native Windows — this is an inference from the dependency list, not a build anybody has run, and it is recorded as such in section 9

The flags note, and the ranking claim it deletes. Earlier editions of this document concluded that because node-pty passes only INHERIT_CURSOR while portable-pty passes INHERIT_CURSOR | RESIZE_QUIRK | WIN32_INPUT_MODE, "a middle layer on portable-pty/psmux/rmux is strictly better on flags than the ADE hosting it". That claim is wrong and is withdrawn. Current ConPTY does not read those bits at all.

[SRC microsoft/terminal src/winconpty/winconpty.cpp] — the complete set of dwFlags consumers in the implementation is three lines:

169: const auto inheritCursor   = (dwFlags & PSEUDOCONSOLE_INHERIT_CURSOR)   ? L"--inheritcursor "   : L"";
170: const auto ambiguousIsWide = (dwFlags & PSEUDOCONSOLE_AMBIGUOUS_IS_WIDE) ? L"--ambiguousIsWide " : L"";
173: switch (dwFlags & PSEUDOCONSOLE_GLYPH_WIDTH__MASK)

[SRC code search over microsoft/terminal] PSEUDOCONSOLE_RESIZE_QUIRK, PSEUDOCONSOLE_WIN32_INPUT_MODE and PSEUDOCONSOLE_PASSTHROUGH_MODE are zero hits each. Three consequences, in order of how much they matter:

  1. Passing 0x2 | 0x4 is a no-op. portable-pty confers no flag advantage over node-pty, and ORCA is not disadvantaged here. Delete the claim wherever it is remembered.
  2. Passing 0x8 is actively harmful. switch (dwFlags & 0x18) matches case PSEUDOCONSOLE_GLYPH_WIDTH_GRAPHEMES (0x08), so anything still ORing the old PASSTHROUGH_MODE value silently selects grapheme-based width measurement. 6.1 states this collision correctly; the 3.7 table used to contradict it.
  3. win32-input-mode as a creation flag is dead; as a mode it is alive. It is negotiated in band at runtime (CSI ?9001h) — the #4999 spec cited in 6.1 describes exactly that negotiation. Earlier editions conflated the dead flag with the live mode and derived a "key-fidelity loss" for ORCA that does not follow.

Method rule this produced, added to 7.2 as rule 9: when you find a flag or a #define, check the consumer, not the header. Absence from a public header proves only "undocumented"; presence in a header proves nothing about whether anything reads it.

3.8 Non-multiplexer channels: observation, control, and hosting

Item What it is Native Win Verdict Evidence
Claude Code hooks Lifecycle hooks with five types, not three: command, prompt (an LLM evaluates the decision), agent (spawns an agentic verifier), http (POSTs event JSON to a URL, reads the decision from the response), mcp_tool (calls a tool on an already-connected MCP server), plus an allowedHttpHookUrls policy allowlist Yes VIABLE-WITH-CAVEAT — a write path, not a tap, and http is not available at bootstrap [DOC code.claude.com/docs/en/hooks:134] "The menu displays all five hook types: command, prompt, agent, http, and mcp_tool" — earlier editions said three. PreToolUse returns permissionDecision of allow/deny/ask/defer plus updatedInput which replaces a tool's arguments. Verbatim: "AskUserQuestion and ExitPlanMode require user interaction and normally block in non-interactive mode with the -p flag. Returning permissionDecision: "allow" together with updatedInput satisfies that requirement: the hook reads the tool's input from stdin, collects the answer through your own UI, and returns it in updatedInput so the tool runs without prompting." Two scope limits earlier editions omitted, both load-bearing: (a) [DOC hooks:549, repeated at :221] "SessionStart and Setup support command and mcp_tool hooks. They don't support http, prompt, or agent hooks" — so the two bootstrap events a middle layer most wants to hook are exactly the ones where the http endpoint trick is unavailable; use mcp_tool or command there. Thirteen events do support all five, including PreToolUse, PermissionRequest, Stop, SubagentStop and UserPromptSubmit, so the technique is intact where it counts. (b) [DOC hooks:328] "As of v2.1.199, an MCP tool whose server marks it with _meta["anthropic/requiresUserInteraction"] is stricter: a hook can't skip its approval prompt with 'allow', with or without updatedInput" — the headless-answering trick has an explicit opt-out that tool authors control. The middle layer can still BE the HTTP hook endpoint for the thirteen supporting events — a long-lived process it owns, sidestepping the stdio-MCP lifetime problem entirely
Claude Code terminalSequence Supported escape-emission channel (v2.1.141+) Yes, explicitly VIABLE, narrow [DOC code.claude.com/docs/en/hooks:146,182-192] "Hooks run without a controlling terminal, so writing escape sequences directly to /dev/tty fails. Instead, return the escape sequence in the terminalSequence field and Claude Code emits it for you through its own terminal write path. This is race-free, works inside tmux and GNU screen, and works on Windows where there is no /dev/tty." Allowlist: OSC 0/1/2 (titles), OSC 9 (incl. 9;4 taskbar progress), OSC 99, OSC 777, bare BEL. Restricted "to sequences that can't move the cursor or alter colors" — so OSC 133 is not available, but a hook can push out-of-band structured signals into the PTY byte stream that a nested middle layer parses natively
Claude Code OTel 34 named claude_code.* identifiers — a mix of metrics, events and spans, not 34 events (see 2.6) Yes (TRACEPARENT inherited by Bash and PowerShell subprocesses) VIABLE — best headless observation channel Master switch is CLAUDE_CODE_ENABLE_TELEMETRY=1, not an OTEL_* var; traces additionally need CLAUDE_CODE_ENHANCED_TELEMETRY_BETA=1; the claude_code.hook span needs ENABLE_BETA_TRACING_DETAILED=1 + BETA_TRACING_ENDPOINT + org allowlisting. Content is redacted by default — opt in via OTEL_LOG_USER_PROMPTS, OTEL_LOG_TOOL_DETAILS, OTEL_LOG_TOOL_CONTENT, OTEL_LOG_ASSISTANT_RESPONSES, and OTEL_LOG_RAW_API_BODIES (with a =file:<dir> mode writing untruncated bodies to disk with a body_ref path). Includes claude_code.tool.blocked_on_user — the "is the agent stuck waiting on a human" signal, with no PTY. Caveat: [DOC hooks:152] "Claude Code removes OTEL_* exporter variables from every subprocess it spawns, including hooks" — config does not reach grandchildren
Claude Code JSONL transcripts ~/.claude/projects/<project>/<session>.jsonl, one line per event Yes (Windows path documented) VIABLE It is a tree, not a file [EMP — inspected on this machine, native Windows 11 build 26200]: a sibling <session-uuid>/ directory holds subagents/agent-<id>.jsonl plus agent-<id>.meta.json sidecars containing {agentType, description, toolUseId, spawnDepth}, plus tool-results/ and workflows/. Subagent fan-out topology is readable from the filesystem alone. Undocumented internals — version-fragile. CLAUDE_CONFIG_DIR relocates the whole tree (the per-workspace isolation analogue of QUIL_HOME); cleanupPeriodDays garbage-collects it, so anything treating JSONL as durable needs its own retention
claude -p --input-format stream-json --output-format stream-json --verbose Full-duplex NDJSON over ordinary pipes Yes [DOC code.claude.com/docs/en/cli-reference] for the flags; "identical on Windows and Linux" is [INF] — the flags are documented without platform qualification and the transport is ordinary pipes, but no pass has run this on both platforms and compared output. This is the second finding's load-bearing cross-platform claim and it rests on an inference; see section 9 and Q23 VIABLE — an alternative architecture Companion flags: --include-hook-events, --forward-subagent-text (with parent_tool_use_id), --include-partial-messages, --replay-user-messages [DOC cli-reference]. Honest scoping: this is print/headless mode — it does not attach to an existing interactive TUI. It is a way to build a middle layer that spawns the agent with no PTY at all
anthropics/claude-code#24365 and #6686 Requests for claude serve / ACP-over-network FORECLOSED Both closed as not_planned. #24365 auto-closed by a stale-bot 2026-03-14, locked 2026-03-21, 3 total reactions, 3 of 4 comments are bots. #6686: 551 reactions, closed not_planned 2026-02-09. Judgement: Anthropic has declined this twice. Betting on native network-attachable Claude Code sessions is a weak hypothesis — which strengthens the case for an external middle layer.
Codex exec --json + rollout JSONL ~/.codex/sessions/YYYY/MM/DD/rollout-*.jsonl, resume --last Yes VIABLE Cross-vendor convergence with Claude Code on the identical shape is itself evidence this is a real architectural pattern, not one company's idiosyncrasy
OpenCode opencode serve Headless HTTP + OpenAPI 3.1 + GET /global/event SSE + /tui/* to drive the TUI Yes VIABLE [DOC opencode.ai/docs/server] "When you run opencode it starts a TUI and a server. Where the TUI is the client that talks to the server… This architecture lets opencode support multiple clients and allows you to interact with opencode programmatically." --port/--hostname/--cors/--mdns, OPENCODE_SERVER_PASSWORD
OSC 133 semantic prompts Prompt-start/end, output-start, command-finished+exit-code Yes — including cmd.exe, via Clink + oh-my-posh [DOC ohmyposh.dev/docs/configuration/general] shell_integration "Works in bash, cmd (Clink v1.14.25+), fish, powershell and zsh" CONTEXT But Claude Code does not emit it — ≥5 duplicate requests since May 2025, all bot-closed. And a nested layer cannot forward it: [DOC tmux#5237, maintainer nicm] "If tmux writes a prompt at 10,10 and you drag the pane so it is now at 15,15, how does tmux tell the terminal it has moved?… The sequences are not powerful enough… they are meant for shells, not full screen programs." A nested layer must parse-and-re-expose, not forward. tmux's own OSC 133 events (pane-command-started etc.) are in unreleased master only — latest release is 3.7b
Watfaq/PowerSession-rs asciicast recorder on ConPTY Yes VIABLE, narrow MIT (verified three ways; the README's "GitBook Community License" footer is a theme artifact). 295★, 2026-07-24. Windows-ONLY (no impl_unix, CI is windows-latest only). server is a config setter, not a daemon — it hosts nothing. Defect found at source [SRC Watfaq/PowerSession-rs, the ConPTY backend under src/ — the exact file and line were not recorded when the finding was made and were not recovered this pass; [UNV]on the citation,[SRC] on the finding]: it calls CreatePseudoConsole once, sized from GetConsoleScreenBufferInfo at startup, and never calls ResizePseudoConsole anywhere (a repo-wide symbol search returns zero hits) — so a mid-recording resize of the outer ADE pane cannot propagate. Open issue #272: may not honour ASCIINEMA_SERVER_URL, and upload is not gated behind auth — a recorder captures everything that transits the PTY, including secrets
faressoft/terminalizer recorder Yes ("Cross platform", default Windows command powershell.exe, @homebridge/node-pty-prebuilt-multiarch) CONTEXT 16,137★ but last push 2024-08-29 and it pulls in electron ^25
asciinema / termtosvg recorders No REFUTED [ART gh api repos/asciinema/asciinema/releases/tags/v3.2.1] asciinema v3.2.1 release assets are darwin/linux only — even in the from-scratch Rust rewrite. termtosvg is self-declared Unix-only and read-only since June 2020
PowerShell Start-Transcript flat text log Yes CONTEXT No timing, no ANSI, no replay. Hooks the PowerShell host, not the PTY — coverage of a native console subprocess's direct writes is host-dependent
reubenlavin08/cc-discord-remote The only working end-to-end late-attach control loop found anywhere Yes Sole existence proof, unvalidated MIT, 0 stars, 0 forks, single author, created 2026-05-16, last push 2026-06-08 (dormant). bot.py is one 159 KB file. Cite for its engineering notes, not as a dependency — see 6.4
nvaccess/NVDA winConsoleHandler.py Production late-attach reader Yes CONTEXT AttachConsole(processID) against a console NVDA never spawned, + GetConsoleProcessList(2) liveness, + ReadConsoleOutputCharacter, in one of the most widely deployed OSS Windows apps. Actively maintained through Oct 2025. Reads only — never writes.
GitHub Actions runner / Buildkite agent Long-poll HTTPS job queues, installable as a Windows Service Yes VIABLE, different shape Trade-off: no live-attachable PTY — output is a streamed log, not a standing shell. actions/runner is MIT, 6,151★, pushed 2026-07-31 — which contradicts a claim that it is near end-of-life. [UNV] That claim was encountered on an unrecorded third-party blog and the source was never captured; the refutation stands on the pushed_at date alone, and the blog should be treated as hearsay rather than cited
LangChain agent-inbox Queue of pending decisions instead of a live terminal Yes REFUTED as an implementation Requires from langgraph.types import interrupt and a running LangGraph deployment. Response discriminator is 'response', not 'respond' (config flags use allow_respond) [UNV — API detail carried from an earlier pass; no file or doc URL was recorded and it was not re-verified]. The pattern is transferable; the implementation is not
RDP / tscon / Terminal Services The in-box Windows tmux-detach analogue Yes CONTEXT — and it breaks a synthesis A disconnected RDP session keeps every process running with a live console; you reattach by reconnecting; tscon moves a session between connections. Gives both persistence and reattach natively, with no third-party code. Limits: whole-session granularity (no per-worktree agent tabs), [DOC learn.microsoft.com/windows-server/…/tscon] "You can't connect to the console session", requires Full Control or Connect access, one interactive session on client SKUs
Emacs as supervisor (vterm / eat / term-mode) No REFUTED at the root [DOC akermu/emacs-libvterm README] "Vterm is not for you if you are using Windows". Root cause: [DOC debbugs.gnu.org bug#71472] Emacs core has never had a real Windows pty; the ConPTY patch (author Ke Wu, not Eli Zaretskii) was de-tagged from patch on 2025-02-12 and sits dormant at wishlist severity
dakra/ghostel.el Emacs terminal on libghostty-vt with a Zig native module Yes, confirmed VIABLE — the one thing that solved the Windows-PTY-under-an-editor problem [DOC CHANGELOG.md [0.43.0] 2026-07-11] "Native Windows support for x86_64 and ARM64 Emacs through a ConPTY backend"; "Windows releases now bundle the native module and Microsoft's redistributable ConPTY runtime… with the system ConPTY as a fallback". 816★, v0.48.0 (2026-07-29), src/ConPtyProcess.zig, every release ships ghostel-conpty-*.dll + ghostel-openconsole-*.exe. Architecturally it is your shape: the native module owns ConPTY; the host application is a swappable frontend
mosh / WinMosh / Eternal Terminal state-sync shells No server on Windows REFUTED [DOC mosh.org] "There is no 'native' mosh executable for Windows available at this time." WinMosh is client-only ("Requires a mosh-server running… on the remote host"). Eternal Terminal is WSL-only by its own README
Windows Service / Scheduled Task hosting persistence without reattach Yes VIABLE, with an unraised showstopper A service runs in session 0 as SYSTEM while ORCA and the human run in session 1+. (a) [INF] Console objects are per-session and AttachConsole attaches by PID, so the mechanism should not be able to cross the session-0 boundary — this is an inference from two documented facts, not an observed failure, and it is recorded as such in section 9 alongside the structurally identical foreign-SID inference. (b) [DOC] Agent CLIs authenticate from the user profile (~/.codex/auth.json, ~/.claude/), which a SYSTEM service does not have. A Scheduled Task avoids (b) only if it runs as the user rather than as SYSTEM

4. Middle-Layer Fitness

Judged on the things that actually matter for a wrapper an ADE spawns: does it survive the ADE dying; can it be driven with no human attached; do its verbs work with no console at all; does it behave inside a nested PTY; is its interface a stable contract; can you install it.

About the "Verbs with no console" column. This is the headless-first constraint stated as a testable property, and it is the one column where the honest answer is almost always untested. zellij action, tmux send-keys, wezterm cli and the rest are normally invoked from inside a terminal; whether they still function when a Windows Service, a Scheduled Task, or a Claude Code hook invokes them with no console attached and no TTY on stdin/stdout is a different question, and nobody in this research ran it. It is cheap to test (Q23) and disqualifying if it fails, because a middle layer you can only drive from a terminal is not headless-first — it is just a multiplexer.

Candidate Survives ADE death Headless verbs Verbs with no console Nesting behaviour Contract stability Installable standalone Overall
psmux Yes Full (tmux verb set + -CC) Untested Explicit guard + PSMUX_ALLOW_NESTING=1 (note the error message names the wrong variable); pipe mode negotiates size in band tmux-compatible = the most-cloned contract in existence Yes (Windows only) Strongest interface, weakest platform breadth. Also carries the unauthenticated full-duplex cross-session port (3.1 row note) and a cargo audit CI gate no peer matches
Zellij Yes CLI + WASM plugins Untested Configurable (ask/fullscreen/descend/never) — the only one with a policy knob Mature, versioned, protobuf plugin API Yes (msvc zip + MSI, and Linux/macOS) Most mature, and the best-verified Windows claim in this map. The TERM/WT_SESSION gate is a config check, not a hazard — it selects the native-console path, which is implemented
rmux Yes 90+ verbs + typed Rust/Python/TS SDKs Untested Untested Single-author (~936 of ~1000 commits [UNV]); dual licence pair unstated Yes Best-designed transport — the only identity-scoped namespace, plus pipe-instance and impersonation checks; thinnest bus factor
wezterm-mux-server Yes (--daemonize, handled explicitly on Windows) 15 CLI verbs, headless by design Untested Untested nested Versioned varint codec explicitly built for skewed client/server pairs Ships in the Windows zip and installer — but no stable release since 2024-02-03; the rolling nightly tag is the only current Windows path Best engineering; you cannot pin a version and be on Windows at the same time
OpenCode /pty While opencode serve runs (L1+, not L2) REST + WS + replay cursor + ticket auth N/A (network client — the transport is the console-independence) N/A (network client) Undocumented — could change without notice Yes (opencode-windows-x64.zip) Best-shaped API for resumable attach; zero contract guarantee, and its wire is UTF-8 text with a fatal decoder and 64 KiB UTF-16 chunking, so it is not byte-exact
ao pty-host Yes, explicitly (DETACHED_PROCESS, survives daemon exit) GetOutput(lines), Status, Kill, SendMessage N/A (network client) applyLargestLocked resize arbitration Hidden: true cobra subcommand — an internal contract No — vendor the package or run the AO binary (Apache-2.0 makes that clean) Closest architectural match; not a product
herdr Yes CLI + HTTP API + SDKs Untested "Nested launch override | beta" (listed Supported) 23k stars, active Preview channel only on Windows Better placed than earlier editions said: "Local persistent sessions" and "Windows Terminal / PowerShell app attach" are both listed Supported (beta); "Direct terminal attach | unsupported" sits among Unix-specific mechanisms. Confirm the reading (Q2), but it is no longer the map's biggest open risk
oly Yes HTTP/WS + named-pipe IPC Untested No nesting evidence — the copilot/opencode "live agent tests" claim was wrong (see 3.1 row note). What exists is a windows-latest PTY matrix over shells Small, single-author, main stalled 1 month; MIT Yes Demoted. Retains a real windows-latest PTY test matrix and MIT licensing. Disqualifying-adjacent: unauthenticated pipe with no ACL. And its output-copilot.expected.windows fixture is direct evidence of platform-divergent rendering, against constraint 3
quil Yes MCP (18 tools) — the richest agent-facing surface Untested Mouse-mode forwarding for nested alt-screen apps Single-author, ~1 release/day, zero external issues ever; MIT Yes Best agent interface; zero Windows CI, AF_UNIX with a no-op chmod, and an unconditional os.Remove on the socket path that bricks a live daemon
qscreen Yes qscn verbs + AttachMode::Bytes Untested Untested 6 stars, zero CI, zero external engagement ever; MIT Yes UNVALIDATED on evidence, not refuted on design. Anti-squatting + per-user namespacing are genuinely right; nobody has ever run it but the author
upterm No — L1 only SSH + public-key auth N/A (SSH is the transport) Ignores os.Interrupt deliberately for ConPTY nesting Apache-2.0, active Yes (scoop) REFUTED. Best auth model in the census; no detach/reattach — that is the disqualifier, and it is a hard one
No-PTY architecture (claude -p stream-json + HTTP hooks + OTel) N/A — you own the process Near-complete: bidirectional NDJSON, permission decisions, updatedInput, 34 typed identifiers Yes by construction — ordinary pipes, no console needed anywhere. The only row that passes this column outright No PTY, no nesting problem at all Vendor-controlled, but publicly documented N/A Best headless story by a wide margin. Two caveats now named: http hooks are unavailable on SessionStart/Setup, and requiresUserInteraction tools can refuse the headless-answer trick. Cannot show a human a live screen

4.1 The four things a middle layer must get right on Windows

1. Answer ConPTY's startup handshake, or eat multi-second stalls. DA1 is the CSI c device-attributes query — "what kind of terminal are you?" — which a terminal is expected to answer.

Citation status first, because this document's own method rule 1 requires it and earlier editions did not disclose it. All the quotes below are verbatim and were read in full, but they live in comments on microsoft/terminal#7019, which is closed as not_planned since 2023-09-29, was created 2020-07-22, and is titled "conpty exhibits pathological performance on scrolling region redraw (repaints entire screen)". Post-closure comments from maintainers are legitimate evidence; the state is disclosed so nobody mistakes this for an open, tracked commitment.

[DOC lhecker (MSFT), microsoft/terminal#7019 comment 2026-06-10] "it sends you a DA1 request, which you're expected to respond to, because you're using a PTY, not just setting up any random pipes. It waits 3s for a response. It's not multiple queries with 1s waits each." [DOC DHowett (MEMBER), same issue] "If you have the ability to answer them, answer them. If you do not… that would mean you are not a terminal emulator."

A measured data point in the same thread took startup from 2121 ms → 142 ms by answering instantly. Attribution correction: that measurement is a third-party application developer's (commenter Eyalm321), not a Microsoft benchmark — earlier editions placed it among Microsoft attributions where readers would reasonably assume otherwise.

oly and psmux both answer the handshake (extract_query_responses_no_client; tests-rs/test_cpr_responder.rs) [UNV — the oly symbol was recorded without a file path in an earlier pass and was not relocated this pass]. Judgement: this is the single most load-bearing design requirement in the whole map — get it wrong and every spawn costs seconds, or hangs.

2. Do not set PSUEDOCONSOLE_INHERIT_CURSOR unless you answer ESC[6n fast. ESC[6n is CPR, the cursor-position report request; the reply is CSI row ; col R. [SRC psmux crates/portable-pty-psmux/src/win/psuedocon.rs:125-133], with a dedicated regression test: "With it, conhost emits an ESC[6n cursor-position request at startup and will not service a child's console connection until the host answers it. So if that reply is sent later than the child's connect attempt, the child blocks in ConsoleCreateConnectionObject during process initialization (a single thread, before any user code runs) until the reply arrives: a temporary stall if it is merely late, indefinite if it never comes." WezTerm sets it; node-pty sets it when inheritCursor is passed.

3. Load a bundled conpty.dll by absolute path, or not at all. [SRC psmux psuedocon.rs:45-51] refuses sideloading entirely: "terminal emulators like WezTerm bundle their own conpty.dll + OpenConsole.exe, and the DLL search order can pick those up when psmux runs inside such a terminal. Using a foreign conpty.dll causes blank panes and broken I/O." Resolution: the hazard is real but specific to how you load. WezTerm uses a bare relative name (Path::new("conpty.dll")) — hijackable. quil (filepath.Join(filepath.Dir(exe), "conpty.dll")) and node-pty (PathCombineW(currentDir, L"conpty\\conpty.dll")) use absolute paths and are safe. Note node-pty resolves a different export name on the bundled path (ConptyCreatePseudoConsole vs kernel32's CreatePseudoConsole), and ClearPseudoConsole is bundled-only.

4. Resize does not propagate by itself. There is no Windows analogue of SIGWINCH. ResizePseudoConsole(HPCON, COORD) is an explicit call by whoever owns the handle, and a child console app only learns of it by reading WINDOW_BUFFER_SIZE_EVENT off its console input buffer — which a byte-stream-only consumer never sees. Two shipping solutions exist: psmux's pipe mode asks the outer terminal in band via XTWINOPS CSI 18 t ("how big are you?") and waits 500 ms for CSI 8 ; rows ; cols t, falling back to 120x30 [SRC src/ssh_input.rs:2016-2039, src/main.rs:4112-4130]; Zellij polls crossterm::terminal::size() at 50 ms on the VT-reader path [SRC zellij-client/src/os_input_output_windows.rs:95 doc-comment, :176 sleep] — and separately runs a 100 ms poller at :38/:52. Open risk: whether Electron/xterm.js answers CSI 18 t by default is unknown — xterm.js gates windowOptions behind opt-in flags. If it does not, psmux's pipe mode silently degrades to a hardcoded 120x30. See also the node-pty ConPTY ABI drift noted in 3.7, which bears on this item: a stale one-argument ConptyClearPseudoConsole prototype against a two-argument export is exactly the kind of mismatch that surfaces as a resize or clear that silently does nothing.

4.2 The nesting hazard that matters most

[SRC psmux src/main.rs:4188-4193], maintainer's own words — quoted with its antecedent restored, which earlier editions dropped: "When running over SSH with a ConPTY console, Windows ConPTY silently consumes DCS escape sequences (including the \x1bP1000p that iTerm2 uses to detect tmux control mode) and also interleaves its own cursor positioning sequences into the output, corrupting the line-based protocol." The remedy the source gives is likewise SSH-specific: "the SSH client must disable PTY allocation so that stdin/stdout are raw pipes: ssh -T user@host tmux -CC."

Judgement, and it is an inference, not the source's claim. The mechanism — ConPTY eating DCS and injecting cursor sequences — plausibly generalises to any ConPTY between a -CC server and its client, including a node-pty one. But the source does not say that, and earlier editions presented the generalised version as a [SRC] quotation. So: [INF] if ORCA spawns the middle layer on a node-pty ConPTY, -CC control mode is likely degraded, and it wants raw pipes. This is empirically testable in an afternoon (Q1) and it determines whether the strongest interface in the map is usable in your actual topology.

And the remedy may not be reachable from ORCA, which is the question Q1 does not ask. ORCA spawns through node-pty, i.e. always a ConPTY. The -CC fix is "give the child raw pipes". So before asking whether corruption happens, ask whether the cure is available at all: can agentCmdOverrides produce a raw-pipe child? If it cannot, then the corruption question is moot and the strongest interface in this map is unavailable in this topology regardless of Q1's outcome. That is filed as Q1a, and it should be answered first because it is cheaper and it can make Q1 unnecessary.

Second-order: [DOC psmux docs/control-mode.md:391] "ConPTY may normalize line endings and process certain cursor movement sequences internally. %output data may look slightly different from what a Unix tmux session would produce" — a documented Windows/Linux behavioural divergence against the cross-platform-uniformity goal.

Third: [SRC psmux src/server/mod.rs:1225] does String::from_utf8_lossy(&bytes) on each ring drain before escaping. A multi-byte character split across two drains becomes U+FFFD permanently. Agent CLIs emit heavy Unicode — the in Claude Code's own window title already broke quil's emulator. Note this is a bytes-to-string bug, not a double-parse bug — and the same class of bug is present in OpenCode's /pty wire (2.2), so it is a property of string-typed terminal transports generally, not a psmux defect.

4.3 Prefix keys in a nested pane — the least-researched nesting concern

Five nesting concerns matter (see 0.1). Four are covered: double parse in 2.2, resize in 4.1 item 4, control-mode corruption in 4.2, and alt-screen/mouse glancingly via quil's mousemode.go. The fifth — whether a multiplexer's prefix key eats the ADE's keybindings, and whether it can be rebound or disabled headlessly — is the thinnest-evidenced part of this map, and it is a daily-annoyance-grade problem rather than a correctness one.

Candidate Has a prefix? Rebindable? Disable-able for headless use?
psmux Yes, tmux-style C-b [INF] yes — tmux-compatible config implies set prefix, not verified [UNV]
Zellij Yes, Ctrl modes rather than a single prefix [INF] yes — keybindings are a first-class KDL config section [UNV]
rmux Yes, tmux-compatible [INF] yes, same reasoning as psmux [UNV]
herdr Yes [UNV] — but note its Windows-beta page lists "Prefix input-source switching | unsupported" [DOC windows-beta.mdx], which is the only primary-source statement about prefixes on Windows found anywhere in this research [UNV]
qscreen, oly, quil, ao pty-host, OpenCode /pty [UNV] — single-pane or network-attached designs may have no prefix at all [UNV] [UNV]
docker attach (as a design reference) Yes, CTRL-p CTRL-q Yes, documented--detach-keys, per-container or global [DOC docs.docker.com/reference/cli/docker/container/attach/] Yes, by remapping to an unused chord

Judgement: the only documented remapping mechanism in the entire census belongs to docker, not to any multiplexer candidate — every multiplexer's rebindability above is an inference from "it has a config file". For a headless-first middle layer the right target is stronger than rebinding anyway: no prefix at all on the headless path, with the prefix existing only when a human attaches. None of the candidates is known to offer that. This is recorded as a genuine gap rather than resolved, and it is the substance of Q24.


5. The Interface Options Map

The brief was to map interface shapes rather than pick one. Seven exist. They are not mutually exclusive — the best-shaped candidates offer two or three.

Shape What it buys What it costs Who offers it Windows
CLI verbs Trivially scriptable from anything; no client library; survives being called by a shell, a hook, an agent, or a human; composes with --json Process-spawn latency per call; no push/streaming; state must be re-fetched tmux, psmux, rmux (90+), wezterm cli (15), ao (thin client over HTTP), Pane runpane (all --json), qscreen qscn, herdr, Zellij All yes
tmux control mode (-CC) A real, decade-hardened, multi-implementer contract for "parse once, let the last hop render"; %output carries raw pane bytes so no double parse Line-based text protocol is fragile to interleaving; ConPTY corrupts it when nested tmux, psmux, WezTerm, ivyterm, par-term psmux and WezTerm yes
Binary wire protocol Version-skew tolerance (WezTerm's varint framing is explicitly designed for it); low overhead; exact byte fidelity You must write and maintain a client; no ecosystem WezTerm codec, ao pty-host proto.go, qscreen ScreenFrame, asciinema ALiS All yes
HTTP + WebSocket Every language has a client; auth/TLS/proxying are solved; SSE or WS gives push; OpenAPI gives a generated client Port management, bind-address and auth become your problem; a stray 0.0.0.0 is a real leak OpenCode /pty (REST + WS + replay cursor + ticket auth), oly, kandev ws://localhost:38429/ws, terminado REST+WS, Warp local_control /v1/control, x3270 -httpd, AO /api/v1/* All yes
MCP The agent can drive it itself, with no glue; discoverable tools A stdio MCP server cannot own persistent sessions — the client spawns it. Only a server the middle layer runs independently works. No terminal-tool naming convention exists anywhere quil (18 tools), kandev (External MCP mode explicitly "for a client outside a task"), paseo daemon, vibe-kanban (dead), ripple Yes
JSON-RPC over stdio/pipe The agent-CLI ecosystem is converging here; transport-agnostic by spec Subprocess-bound lifetime unless you add a network transport ACP (spec sanctions custom transports — ACP over a named pipe is conformant), Codex app-server (stdio/uds/ws/remote_control) ACP: no Windows CI. Codex: transport yes, daemon lifecycle no
Structured event stream (no control plane) Zero PTY; survives ADE swaps for free; vendor-maintained Read-only for observation; write path is a separate mechanism Claude Code OTel (34 events) + JSONL tree; Codex rollout JSONL; OpenCode /global/event SSE Yes

5.1 What an ADE-agnostic middle layer would have to expose

Judgement, derived from the union of every candidate's verb set (runtimeselect.Runtime in AO, wezterm cli, tmux/psmux/rmux verbs, OpenCode /pty, quil's MCP tools):

Verb Why it is non-negotiable
spawn(cmd, cwd, env, cols, rows) → session_id The ADE hook point. ORCA's agentCmdOverrides is exactly this
list() → [{id, alive, pid, exit_code, cwd, title}] Discovery without state in the ADE
attach(id, {cursor}) → {replay, cursor, stream} The one primitive nothing else substitutes for. Must carry a resumable cursor — OpenCode's protocol.ts is the model for the cursor semantics only; copy its absolute-output-cursor control frame, not its wire type. Its payloads are JS strings, so it splits surrogate pairs at 64 KiB and drops invalid UTF-8 input (2.2). Carry bytes, not string
detach(id) Distinct from kill; the thing dtach/abduco exist for
send(id, bytes) and send_keys(id, keys) Raw bytes for fidelity, named keys for scripts
get_output(id, lines) / snapshot(id) → {text, cursor_x, cursor_y} Headless read without attaching. quil's MsgScreenshotPaneResp is the shape
resize(id, cols, rows) Must be explicit on Windows. Multi-client needs an arbitration rule — AO's applyLargestLocked is the only one found
status(id) → {alive, pid, exit_code, blocked_on_user?} Headless liveness. The blocked_on_user bit is what OTel gives you free
kill(id, signal) With a process-tree story: Job Objects on Windows (upterm, rmux, kandev's cmd/winjob/), not bare TerminateProcess
wait(id, {idle|text|exit}, timeout) The primitive that makes it scriptable. boo, rmux and ht all shipped this independently

Two properties beyond the verb set, both learned the hard way:

  • Namespacing must be identity-scoped, not merely selectable. rmux's \\.\pipe\{prefix}-{sid}-il-{integrity}-{label} is the only correct implementation found, because the name is computed from the token rather than picked from a string. oly is the counterexample, but for a sharper reason than earlier editions gave: it does have a name knob (OLY_SOCKET_NAME), and a knob is not access control — its default is machine-global and its pipe carries no ACL and no FILE_FLAG_FIRST_PIPE_INSTANCE. Namespacing prevents accidents; ACLs prevent attacks. You need both, and only rmux has both.
  • A local transport is not an auth boundary. psmux's auth is per-user, not per-process: ORCA, every agent CLI, every MCP server and every npm postinstall script run as the same Windows user and can all read the key file. That is the same trust model as tmux's 0700 socket — a category property, not a psmux scandal. But note the asymmetry: a Unix socket's mode is kernel-enforced at connect time, whereas a TCP loopback port is reachable by any local process, including other users and low-integrity ones, with only a 64-bit non-CSPRNG secret in a default-ACL file in the way. A properly-ACL'd named pipe with FILE_FLAG_FIRST_PIPE_INSTANCE is strictly stronger.

5.2 The reframe worth taking seriously

[DOC opencode.ai/docs/server] "When you run opencode it starts a TUI and a server. Where the TUI is the client that talks to the server." [DOC evennia.com/docs] Portal owns every connection; Server owns all logic and can be reloaded with clients still attached. [DOC spec.nrepl.org] the frontend is disposable; the state lives behind a socket.

Three unrelated ecosystems — a coding agent, a MUD engine, and a Lisp REPL — independently arrived at the same shape. Judgement: that convergence is the strongest architectural signal in this entire map, stronger than any individual tool finding. The name for what you are building is Portal.


6. Negative Results and Dead Ends

6.1 ConPTY facts that will mislead you if you re-derive them

  • PSEUDOCONSOLE_PASSTHROUGH_MODE = 0x8 is a stale dead define, and passing it is actively harmful. It is defined-and-unused in microsoft/node-pty (src/win/conpty.h:27) and marked #[allow(dead_code)] in wezterm's portable-pty. [SRC microsoft/terminal src/inc/conpty-static.h] the current public flag list is INHERIT_CURSOR (0x1), GLYPH_WIDTH__MASK 0x18 / GLYPH_WIDTH_GRAPHEMES 0x08 / GLYPH_WIDTH_WCSWIDTH 0x10 / GLYPH_WIDTH_CONSOLE 0x18, AMBIGUOUS_IS_WIDE (0x20). Bit 0x08 now means grapheme width measurement, so anything still ORing the old passthrough value silently opts into grapheme-based text measurement. A global code search finds the stale symbol in 148 files, including psmux, Sora-bluesky/winsmux, aws/amazon-q-developer-cli, and vibetunnel — it is propagating through the exact candidate pool.
    • Reasoning correction, because the argument earlier editions gave for this was invalid. The claim was justified by "it is absent from the public header". That inference does not work: 0x2 (RESIZE_QUIRK) and 0x4 (WIN32_INPUT_MODE) are equally absent from that header, and this document treated those as live features elsewhere. Absence from a public header proves only "undocumented". The decisive evidence is the consumer, not the header: [SRC src/winconpty/winconpty.cpp:169-186] reads exactly three things out of dwFlagsINHERIT_CURSOR, AMBIGUOUS_IS_WIDE, and the GLYPH_WIDTH__MASK switch — and RESIZE_QUIRK, WIN32_INPUT_MODE and PASSTHROUGH_MODE are zero hits across the whole repository. That is what makes 0x2 and 0x4 no-ops and 0x8 a mis-selection. Full argument and its consequences for candidate ranking: end of 3.7.
  • The good news is the inverse of what you would expect. [SRC microsoft/terminal PR #17510, merged and closed 2024-08-01T20:38:11Z] "A minor ConPTY refactoring: Goodbye VtEngine Edition" — "any VT output that an application generates will now be given to the terminal unmodified." Microsoft shipped the passthrough outcome as the default, and issue #1173 closed as completed at 2024-08-01T20:38:13Ztwo seconds later, not "seven minutes" as earlier editions said. (The seven-minute figure is real but belongs to a different issue, #5468: created 17:14:15, closed 17:21:46 — see 6.4. It was carried across by mistake.) Since Aug 2024, ConPTY passes application VT output through unmodified by default. The "double VT parsing is unsolved at OS level" conclusion is wrong.
  • But passthrough makes one bug worse. [DOC lhecker (MSFT), microsoft/terminal#7019 — issue closed not_planned 2023-09-29; comment thread remains the primary source] passthrough-mode ConPTY "already injects VT sequences into stdout irrespective of the VT parser state" — which is exactly the mechanism behind [DOC microsoft/terminal#19621, filed by DHowett (MEMBER), open] "This will impact tmux control mode, as ConPTY will send \x1B to terminal and promptly terminate the in-flight exchange. All in-band signaling from ConPTY must be suppressed when passing through a DCS."
  • ConPTY nesting is a designed, negotiated scenario, not an accident. [DOC doc/specs/#4999 - Improved keyboard handling in Conpty, lines 299-322] works through WT → conpty[1] → wsl → conpty[2] → cmd.exe: "Conpty[2] will ask for win32-input-mode from conpty[1] when conpty[2] first boots up. As conpty[1] is just a conhost that knows how to handle win32-input-mode, it will switch its own VT input handling into win32-input-mode." It also confirms the double-parse cost is by design: each layer re-encodes INPUT_RECORDs to VT and back.
  • Still open and unfixable from outside: [DOC microsoft/terminal#15976, promoted to a megathread] ConPTY reflow/resize desync — "we're promoting this to a megathread cause this is a Hard problem". [DOC doc/specs/#13000 - In-process ConPTY, lhecker] names it "unsolvable" in the current architecture: the drift is between ConPTY's buffer and the hosting terminal's buffer — two separate processes — and a named MUTEX is explicitly rejected on ABBA-deadlock grounds. A nested middle layer that keeps its own screen model becomes a third independent representation.
  • [DOC microsoft/terminal#17384, open since 2024-06-06] pasting >5 KiB into a slow-reading app deadlocks the whole terminal; Ctrl-C stops working.
  • [DOC microsoft/terminal#17643] residual VT-passthrough issues still open: COMMON_LVB_GRID_HORIZONTAL → SGR 53 translation dropped, VtIo::Writer::WriteInfos not verifying per-character width, SetConsoleActiveScreenBuffer destroying grapheme clusters. (Cooked-read reflow, ScrollConsoleScreenBuffer, the DA3 truncation and the PSReadLine SGR issue are all fixed — do not carry them forward as hazards.)

6.2 node-pty defects live in ORCA's own stack today

Defect Status Consequence
AttachConsole failed crash on kill of an already-exited shell [SRC PR #886] open since 2026-02-06, unmerged; [SRC PR #901] (Node 22 resize-after-exit) open, unmerged Both surfaced as user-visible crash dialogs in shipping Electron products: openai/codex#25415 and microsoft/vscode#201029
The try/catch guard [SRC commit 2b25c761, v1.2.0-beta.11, 2026-02-03] already merged upstream Codex and VS Code shipped stale bundled copies. The fix is: pin node-pty ≥ 1.2.0-beta.11
Root cause still open [DOC vscode#201029 final comment] "kill() unconditionally tries to enumerate console processes for a shell whose exit has already been handled" — resize() has an _exitCode guard, kill() does not Triggered by short-lived diagnostic shells (cmd.exe /c "exit /b 0") racing the kill path
Data race in ptyHandles [SRC gh api repos/microsoft/node-pty/issues/921 → closed_at 2026-05-13T11:28:17Z] — fixed 2026-05-13, not 05-12 A process-global vector mutated from per-PTY watcher threads; concurrent std::remove_if dereferenced a moved-from unique_ptr. Real hazard at moderate session counts
Conout worker deadlock [SRC PR #943, merged 2026-07-31] A regression of a bug already fixed once (#763 → PR #885 → regression → #943). Triggered when the Node inspector pauses the conout worker thread
ORCA's own instance [DOC stablyai/orca#9586, closed with zero comments and no maintainer reply] The "root cause / fixed in v1.2.0-beta.11" text is the reporter's own unreviewed self-diagnosis, not an established finding

6.3 Non-multiplexer routes: what each one actually gives up

Route Persistence Reattach Verdict
Windows OpenSSH Server No No [DOC Win32-OpenSSH#2291, filed by MSFT member SteveL-MSFT, open] no tmux equivalent exists. sshd puts children in a Job Object; disconnect tears it down — not SIGHUP, so there is no signal to trap. (Caveat: the Job-Object mechanism is asserted in one maintainer sentence; code searches for CreateJobObject/KILL_ON_JOB_CLOSE in openssh-portable return zero — treat as absence-of-evidence, not established.)
Windows Service Yes No Plus the session-0 showstopper: [INF] AttachConsole should not be able to cross it (console objects are per-session, and the call takes a PID) — an inference from two documented facts, never observed to fail; and [DOC] a SYSTEM account has no user-profile agent credentials, which is the half that is established
Scheduled Task Yes No Same, unless run as the user
Hook + file/JSONL IPC Structured replay Not live [DOC Claude Code hooks] resumed sessions replay saved hook output rather than re-running it, so timestamps and commit SHAs go stale
Job-queue fleets (Actions runner, Buildkite, OpenHands) Yes at scale No — a streamed log, not a shell Deliberate trade
RDP / tscon Yes Yes The counterexample that breaks "every route gives up one" — but whole-session granularity only
codex app-server --listen ws:// Yes Standing listener the client does not own The other counterexample — but experimental, and it owns no PTY

[DOC openai/codex#11750, filed by the harness author] is the cleanest evidence that file-based IPC narrows PTY dependency without eliminating it: "the only workaround is spawning codex fork inside a pseudo-TTY, polling the filesystem for new rollout files to discover the forked session ID, killing the TUI… This works but is fragile and slow (~6s overhead), requires PTY bindings (e.g. node-pty)". (Note bearlyai/OpenADE has no LICENSE file and license: null — all-rights-reserved by default, usable as evidence, not as a component. And thread/fork already exists on app-server, so the PTY fallback exists because codex exec lacks a fork verb, not because the capability is intrinsically PTY-bound.)

6.4 Late-attach (AttachConsole) — refuted as a foundation

Microsoft has explicitly told developers not to build on the half you need. [DOC learn.microsoft.com/windows/console/writeconsoleinput] — all four sentences below are verbatim, but they come from two different callouts, and earlier editions attributed all four to the "Important" banner. The Important banner says the API is "console platform functionality that is no longer a part of our ecosystem roadmap". A separate Tip callout in Remarks says: "This API is not recommended and does not have a virtual terminal equivalent… This operation is considered the wrong-way verb for this buffer. Applications remoting via cross-platform utilities and transports like SSH may not work as expected if using this API." The substance is unaffected; the attribution is corrected. [DOC .../classic-vs-vt] names the class: input injection and output scraping "provide a vector to cross security and privilege-levels or domains". AttachConsole itself survives in the still-condoned "exceptions" list for process bookkeeping.

[DOC DHowett-MSFT, microsoft/terminal#5468] — created 17:14:15 and closed Resolution-By-Design at 17:21:46, seven minutes later (this is the issue the "seven minutes" figure belongs to; see 6.1) — "A lower-integrity process is not allowed to read the console output of a higher-integrity console. In general, there is no reason to ever use the ReadConsoleOutput API." Note the rule is directional: same-user, same-integrity siblings (which ORCA's children are) are unaffected. It bites only for elevated agents, cross-user, or session 0.

Nobody has built a general late-attach library, and the split explains why. [DOC pywinauto#492], filed by a pywinauto maintainer in 2018 and still open, contains his own 2022 empirical attempt: AttachConsole succeeded, ReadConsoleOutputCharacter returned ten spaces, and it did not work at all against cmd.exe or PuTTY. The mature implementations all pick one half: NVDA late-attaches but only reads; winpty and wexpect read and write but own the console from birth so they never call AttachConsole. Judgement: every half has a production implementation, and the halves never combine — the ecosystem has already converged on own-from-birth.

The one working full loop, reubenlavin08/cc-discord-remote (MIT, 0 stars, dormant since 2026-06-08), documents its constraints as design workarounds rather than solutions, and those notes are the valuable part: a Win32 process can own only one console, so it spawns a throwaway console_helper.py per operation rather than corrupt its own stdio; ReadConsoleOutputCharacter sees only the visible viewport, never scrollback; concurrent human typing plus programmatic writes garble the JSONL, so "one client per session at a time"; and "the JSONL format and session-registry layout are undocumented internals".

One more structural note: late-attach is not a nested topology at all. It is an out-of-band, sideways-reaching daemon — a different shape from every other candidate in this map, and worth naming rather than silently conflating.

6.5 Standards that do not exist

  • No MCP terminal/PTY convention: zero terminal SEPs among 41 Final ones, dozens of independently-named mcp-terminal-server repos with no shared schema.
  • No second production consumer of tmux control mode was thought to exist. That was wrong — WezTerm, ivyterm and par-term all implement it, and WezTerm exercises it on Windows. But there is still no specification independent of tmux's implementation.
  • No Windows analogue of reptyr, after 10+ years.
  • No compatibility matrix, from Microsoft or anyone, for running a newer bundled conpty.dll/OpenConsole.exe against an older or locked-down Windows Server Core / LTSC host.
  • No primary Microsoft source pins the build threshold for WIN32_INPUT_MODE (spec authored 2020-05-07) or for passthrough (build >= 22621 is corroborated only by two independent Rust implementations, psmux and rmux, converging on the same magic number).
  • No open-source SCADA/industrial session manager with a persistent-session-plus-attach design was located — genuinely under-researched, not confirmed absent.

7. What Was Refuted

Everything here was believed at some point during this research and killed by an adversarial pass. Do not re-import any of it.

7.1 Facts that were wrong

Read the Consequence column first if you are scanning. verdict changed means a candidate moved between VIABLE / REFUTED / CONTEXT because of this — those are the rows that matter for a decision. framing corrected means the conclusion held but the reasoning or scope did not. citation only means a fact was right and its source was wrong — method hygiene, not decision-relevant.

Claim Reality Source Consequence
"quil uses Named Pipes on Windows (per ADR-2)" Never implemented. net.Listen("unix", …) unconditional, no build-tag split in internal/ipc/, no go-winio in go.mod. The claim exists only in prose in quil's own docs/architecture.md:20 [SRC internal/ipc/server.go:311, client.go:15] verdict changed
"psmux uses TCP + named pipes; issue #5 was closed by #13's named-pipe migration" PR #13 was closed without being merged (merged: false). psmux has no named-pipe IPC at all. It kept TCP and bolted on an AUTH key [SRC gh api repos/psmux/psmux/pulls/13 -> merged:false, state:closed] verdict changed
"ConPTY passthrough was prototyped and never shipped; double-VT-parsing is unsolved at OS level" Issue #1173 is closed as completed; PR #17510 shipped the outcome as the default in Aug 2024 [SRC gh api repos/microsoft/terminal/issues/1173 -> closed_at 2024-08-01T20:38:13Z, state_reason completed; pulls/17510 -> merged, closed_at 20:38:11Z] framing corrected
"There is an active, heavily-upvoted open request for Claude Code network attach" #24365 is closed not_planned, stale-bot-closed, locked, 3 reactions, 3 of 4 comments are bots. #6686 also closed not_planned [SRC gh api repos/anthropics/claude-code/issues/24365 -> closed, not_planned, locked:true, 3 reactions, 4 comments; issues/6686 -> closed, not_planned, 551 reactions] framing corrected
"tmux control mode has one production implementer (iTerm2)" At least four more, one of which (WezTerm) ships nightly Windows binaries and has fixed a Windows-specific tmux -CC bug [SRC wezterm mux/src/tmux*.rs, docs/changelog.md] verdict changed
"darktile#288 shows near-zero control-mode adoption" The issue is closed, zero reactions, and darktile itself is dead (last push 2023-03-19). It supports no conclusion in either direction [SRC gh api repos/liamg/darktile/issues/288 -> closed, 0 reactions; repo pushed_at 2023-03-19] citation only
"Vibe Kanban is ~3 months stale" Sunset. Second-to-last commit is "Add README sunsetting banner" [SRC gh api commits, README] verdict changed
"Agent Orchestrator has no CLI/HTTP surface, just a plugin SDK" Full Cobra CLI over a documented loopback HTTP REST API, plus a native ConPTY session-host subsystem, plus Windows npm binary packages [DOC docs/cli/README.md; SRC backend/internal/adapters/runtime/conpty/] verdict changed
"kandev is a web UI with no third-party API" WebSocket API, four MCP modes incl. External MCP, agentctl HTTP route groups, plugin marketplace, service install [DOC docs/public/*.md] verdict changed
"No candidate uses a raw TCP or binary wire protocol as its primary control surface" AO uses loopback HTTP REST for the daemon and a custom binary protocol over loopback TCP for the PTY layer. Warp ships crates/local_control. psmux binds TCP [SRC Untrivial-ai/agent-orchestrator backend/internal/adapters/runtime/conpty/proto.go; warpdotdev/warp crates/local_control/src/catalog.rs; psmux/psmux src/server/mod.rs:839] framing corrected
"asciinema/Windows: no artifact" — but the whole ecosystem is refuted The CLI is refuted; the ecosystem is not. avt (Apache-2.0, deps rgb + unicode-width only) compiles on Windows, and the four ALiS/asciicast WS sub-protocols are fully specified for third-party producers [SRC Cargo.toml; DOC docs.asciinema.org] verdict changed
"No observation channel provides a write path; injecting input requires PTY ownership" Refuted four ways, three of them native-Windows and headless: Claude Code hooks (permissionDecision + updatedInput), claude -p --input-format stream-json, OpenCode's /tui endpoint, and AttachConsole + WriteConsoleInput [DOC hooks, cli-reference, opencode server docs] framing corrected
"Reconstructing on-screen state requires PTY ownership" It requires the byte stream plus a VT emulator. asciinema's relay does exactly this and never touches a PTY: "The server maintains comprehensive state for each active stream by running the whole stream through asciinema's own virtual terminal emulator" [DOC docs.asciinema.org/manual/server/streaming/] framing corrected
"Claude Code's TUI swallows escape codes, so escape-based signalling is impossible from outside" Anthropic deliberately removed the controlling terminal from hooks in v2.1.139 and shipped a supported emission channel (terminalSequence) in v2.1.141. The CLI cooperates — via an allowlist that excludes OSC 133 [DOC hooks:146,182-192] framing corrected
"No cmd.exe path exists for OSC 133" oh-my-posh's shell_integration "Works in bash, cmd (Clink v1.14.25+), fish, powershell and zsh" [DOC ohmyposh.dev/docs/configuration/general -- shell_integration] citation only
"tmux OSC 133 events have shipped" They are in unreleased master (CHANGES FROM 3.7b TO 3.8). Latest release is 3.7b [SRC gh api CHANGES, releases/latest] citation only
"Windows containers, Hyper-V backend of Docker Desktop is viable" Docker Desktop is proprietary and paid above thresholds — refuted on constraint 2, exactly like sbx. Applying the constraint to one and not the other was an internal inconsistency [DOC docs.docker.com/desktop/setup/install/windows-install/ + Docker Desktop subscription terms] verdict changed
"sandbox-runtime and Coder have unconfirmed licenses" Apache-2.0 and AGPL-3.0 respectively, one API call away [SRC gh api repos/anthropic-experimental/sandbox-runtime/license -> Apache-2.0; repos/coder/coder/license -> AGPL-3.0] citation only
"node-pty proves late-attach to a foreign console works" It passes _innerPid — the PID it spawned itself. Nothing anywhere late-attaches to a stranger's console and drives it [SRC src/windowsPtyAgent.ts:149] framing corrected
"Nobody wraps AttachConsole+Read/WriteConsole into a reusable library" NVDA, winpty and wexpect all do — each implementing a different half. The literal conjunction survives on a technicality; the useful finding is the split [SRC nvda source/winConsoleHandler.py; winpty src/agent/*; wexpect console_reader.py] framing corrected
"Superlogical distributes the libghostty VT state machine to clients" The cited primary source contains zero architecture content. That claim traces to secondary journalism. The post's actual decision-relevant fact: Superlogical is a company shipping a commercial product [DOC mitchellh.com/writing/superlogical, full text] verdict changed
"upterm is silent on Windows" It has a ### Windows install section, a closed "Add Windows Support" issue, host/internal/pty_windows.go with real ConPTY + Job Objects, and Windows release artifacts [SRC owenthereal/upterm host/internal/pty_windows.go, host/host_windows.go; ART v0.24.0 upterm_windows_{386,amd64,arm64}] verdict changed
"Zellij's Windows support is unknown" Shipped in 0.44.0 (~2026-03-23), maintained by divens, with MSVC zip + MSI artifacts [SRC zellij-server/src/os_input_output_windows.rs, zellij-client/src/os_input_output_windows.rs; ART v0.44.3 zellij-x86_64-pc-windows-msvc.zip + -installer.msi] verdict changed
"herdr has no primary repo" 23,466 stars, Apache-2.0, github.com/herdrdev/herdr — and its URL was inside an artifact the researcher had already scraped [SRC gh api repos/herdrdev/herdr -> 23,466 stars, Apache-2.0] citation only
"rmux's Windows claim is README marketing" Named-pipe daemon with ImpersonateNamedPipeClient, server_identity_windows.rs anti-squatting validation, SID+integrity-scoped pipe names, and a real ConPTY backend with Job Objects [SRC Helvesec/rmux crates/rmux-ipc/src/stream_windows.rs, server_identity_windows.rs, windows_mutex.rs, crates/rmux-pty/src/backend/windows/io.rs] verdict changed
"bash_kernel is viable" signal.SIGPIPE (nonexistent on Windows) and pexpect.spawn (POSIX-only) in _start_bash(). And it is stale since 2025-01-05 — "updated 23 days ago" was the updated_at metadata field, not pushed_at [SRC kernel.py:116,123,139] verdict changed
"ttyd has partial persistence" Zero. A new process per WebSocket, unconditionally killed on close [SRC src/protocol.c] verdict changed
"Coder's reconnectingpty is a viable candidate" Prior art only: not deployable standalone, AGPL + a proprietary enterprise/ tree, and it self-terminates after 5 minutes with nothing attached [SRC coder/coder agent/reconnectingpty/{reconnectingpty.go,screen.go,buffered.go}; gh api repos/coder/coder/license -> AGPL-3.0] verdict changed
"cmux's license is unknown (NOASSERTION)" GPL-3.0-or-later, stated in its LICENSE file; GitHub returns NOASSERTION only because of a dual-offer preamble [SRC contents/LICENSE] citation only
"PowerSession-rs's license is unclear" MIT, verified three ways [SRC contents/LICENSE, Cargo.toml, repo metadata] citation only
"Nimbalyst's agent extensibility is unresolved" Fixed set of exactly four. Its extension system is for file-type editors, not agents [DOC Nimbalyst README] citation only
"Pane is an independent project" A Crystal fork — it retains docs/CRYSTAL_ARCHITECTURE.md verbatim. Crystal produced two descendants, not one [SRC dcouple/Pane docs/CRYSTAL_ARCHITECTURE.md] framing corrected
"Daytona is open-source infrastructure" Repo root is README.md + assets, license removed, GET /license → 404. "As of June 2026, Daytona's core development has moved to a private codebase" [SRC gh api repos/daytonaio/daytona -> license null; contents/ = README.md + assets; GET /license -> 404] verdict changed
"Sculptor runs agents in Docker containers, license unclear, Windows contradictory" MIT; Docker is the experimental backend (default is worktrees); download list is Mac/Linux/Linux-ARM64 only — no Windows build [SRC gh api repos/imbue-ai/sculptor/license -> MIT; DOC sculptor README download list] verdict changed
"container-use has native Windows support" Compilation only, no installation method (its own follow-up PR says so, open a year); the "drop into any agent's terminal" feature is broken on Windows in every shipped release; last release 2025-08-19 [SRC PR #252, #348, #355] verdict changed
"DevPod's Kubernetes provider gives a non-WSL path" Abandoned. Last release is v0.7.0-alpha.34 (2025-06-23); maintainers publicly reallocated to vcluster [SRC gh api; DOC issue #1946] verdict changed
"The non-WSL paths are consistently the less-traveled option" minikube's own driver index lists Hyper-V as "preferred" on Windows, and Docker's docs state there is no architectural preference between WSL and Hyper-V [DOC minikube.sigs.k8s.io/docs/drivers/ -- both Hyper-V and Docker carry the "preferred" label; docs.docker.com/desktop/setup/install/windows-install/] framing corrected
"Every non-multiplexer route gives up persistence or reattach" RDP/tscon gives both natively; codex app-server --listen ws:// is a standing listener the client does not own [DOC learn.microsoft.com/windows-server/administration/windows-commands/tscon; codex-rs/app-server/README.md] framing corrected
"Every one of the four small candidates leaks orphaned processes on Windows teardown" Supported for psmux, partially for quil, zero evidence for oly, and necessarily zero for qscreen (which has no users to file reports) [SRC gh api issue searches across psmux/psmux, artyomsv/quil, slaveOftime/open-relay, dualface/qscreen] framing corrected
"psmux's star count shows astroturfing" The repeated promotional sentence is psmux's GitHub repository description, reproduced verbatim by aggregators and link-preview cards — including on a fork's page. Not evidence of a campaign [SRC gh api repos/psmux/psmux -> the repeated sentence is the repo description field, reproduced verbatim by aggregators and on a fork's page] framing corrected
"No independent field report exists for psmux" [2ND laurentkempe.com/2026/03/31/from-3-worktrees-to-n-ai-powered-parallel-development-on-windows/] — an independent developer's field report (earlier editions called the author "a .NET MVP"; the string "MVP" does not appear on the cited page and that descriptor is withdrawn), multi-week use across projects, Copilot CLI + worktrees on native Windows: "I can detach from a session, close my terminal, come back hours later, and reattach to find everything exactly where I left it — agents still running, output still visible… No WSL required. No dependencies." Tagged [2ND]: a user's blog, which is the correct tier for a field report and the strongest available evidence for "somebody other than the author runs this in anger" [2ND laurentkempe.com/2026/03/31/from-3-worktrees-to-n-ai-powered-parallel-development-on-windows/] citation only
"ACP defines no network transport and has no detach/reattach primitives" Its Transports chapter names Streamable HTTP and formally sanctions custom transports; session/load, session/resume, session/list, session/fork, session/close all exist. (The conclusion holds — those are conversation-state, not live-process attach — but the flat assertions were false) [DOC docs/protocol/v1/draft/transports.mdx; SRC agent.rs:4681-4700] framing corrected
"Warp's Oz API is cloud orchestration only" crates/local_control is an open-source local control plane with ~60 implemented actions over authenticated loopback HTTP. (The substantive point survives: no output-read, no send-keys, no attach) [SRC warpdotdev/warp crates/local_control/src/catalog.rs:4 and the full action catalog -- 84 entries marked status: Implemented] framing corrected
"microsoft/terminal#5468 is an integrity-level bug" Closed Resolution-By-Design in 7 minutes. It is a permanent security-model constraint, and directional — it does not bite same-user same-integrity siblings [SRC gh api repos/microsoft/terminal/issues/5468 -> created 17:14:15, closed 17:21:46, label Resolution-By-Design] framing corrected
"The double-parse is unavoidable with these candidates" qscreen AttachMode::Bytes, oly WS Data, quil MsgPaneOutput and psmux -CC %output all ship raw bytes today [SRC qscreen crates/qscreen-protocol/src/lib.rs:56-61; oly src/http/ws.rs:36-46; quil internal/ipc/protocol.go:204-208; psmux src/server/mod.rs:1222-1227] verdict changed
"ripple's HANDOFF_GARBLING.md shows a rendering multiplexer corrupting output" It shows the opposite. The broken component was CommandOutputRenderer, a naive logical-line model with MaxCol=100,000 and no grid. The doc states the VT-emulating path "was OK from the start" [DOC HANDOFF_GARBLING.md:42,103] framing corrected
"quil's 0x9C-in-UTF-8 bug proves double-parse corruption" Its own techdebt doc classifies it as an upstream charmbracelet/x/vt spec-compliance defect with a known correct general fix, already worked around, found on macOS [DOC techdebt/3-3-*.md] framing corrected
"TelePharo is a thin but live lead" No code activity since 2022-12-23. Abandoned, same class as pymux [SRC gh api — repo metadata for the projects named in this row] verdict changed
"Toad's Windows status is undefined" "Toad runs on Linux and macOS. Native Windows support is currently lacking… but Toad will run quite well with WSL" — refuted under the no-WSL constraint [DOC README:175-177] verdict changed
"Zellij and the libghostty consumers mean no lineage tool is native-Windows" Zellij, rmux, herdr and WezTerm all are [SRC zellij, Helvesec/rmux, herdrdev/herdr, wezterm/wezterm -- all four verified at source in 3.1] verdict changed

7.2 Method failures worth institutionalising

These recurred across angles and each one produced a wrong conclusion:

  1. GitHub artifact status was assumed, not checked. Six load-bearing citations turned out to be closed, not_planned, unmerged, or filed against a dead repo. Any issue or PR cited as evidence needs {state, state_reason, merged, closed_at, reactions, repo pushed_at} pulled.
  2. updated_at was read instead of pushed_at. The former ticks on stars and watches. This alone flipped bash_kernel from "19 months stale" to "updated 23 days ago".
  3. Vendor docs were trusted over source for feature inventory. opencode.ai/docs/server omits the entire /pty API. docs.warp.dev omits local_control. Both projects are open source; the source was one API call away.
  4. A README grep was treated as conclusive. It is the weakest of four available signals. upterm failed the grep on a heading the grep should have caught and passed the source tree, the CI config, the release artifacts and the issue tracker.
  5. "Windows release artifact exists" was treated as proof of a working Windows daemon. ttyd is the counterexample: real ConPTY in source, and a shipped binary that cannot spawn a child on your exact OS build. Check that the released artifact is recent enough to contain the fixes in master.
  6. A cited URL did not contain the quoted sentence. This happened at least five times (terminal#1173, learn.microsoft Q&A 5789906, the window-station page, node-pty#886, abduco's project page). Conclusions mostly survived; citations mostly did not.
  7. Stopping at the first file that answers the question. wetty's login.ts has the POSIX hardcode — and so does spawn.ts, the actual main path. Grep the whole tree.
  8. When a source page contains both a "supported" and an "unsupported" table, quote both. herdr's Windows-beta page was read for its Not supported table and the Supported table above it went unread — which turned "local persistent sessions and app attach both work in beta" into "the exact path you need is the one thing not working", and made Q2 the map's top open question for two editions. If a page is structured as a capability matrix, the negative half is not the page.
  9. When you find a flag or a #define, check the consumer, not the header. The whole "portable-pty is strictly better on flags than node-pty" conclusion rested on reading two headers and never grepping microsoft/terminal for who reads dwFlags. Absence from a public header proves only "undocumented"; presence in a header proves nothing at all. One code search over the implementation refuted a claim that had propagated into candidate rankings.
  10. A fixture file is not a test. tests/output-copilot.log in oly's tests/ directory was read as evidence of live agent-CLI e2e tests. It is a recorded ANSI dump consumed by a different module's unit tests. Before citing a test artifact, find the code that opens it.
  11. A CVE number is a claim like any other and must be fetched. "Documented RCE (CVE-2021-30481)" was carried for an entire research cycle attached to the wrong protocol. One NVD lookup refuted it.

8. Open Questions

Each with a concrete next step, a cost, and — new in this edition — what the answer changes. Ordered by decision impact, not by topic. The first four can each eliminate whole branches of the map; the last group is due diligence.

Tier 1 — answers that eliminate branches. Do these first.

# Question Next step Cost If yes → / If no →
1a Can ORCA's agentCmdOverrides produce a raw-pipe child at all? ORCA spawns through node-pty, i.e. always a ConPTY, and the documented -CC remedy is "give the child raw pipes". Ask whether the cure exists before measuring the disease. Read ORCA's spawn path from tui-agent-launch-command.ts through to the node-pty call; check whether any override shape bypasses the PTY. 2 hours Yes → Q1 is worth running and -CC stays in play. No → -CC control mode is unreachable in this topology whatever Q1 finds; psmux drops to its plain CLI verbs and the "strongest interface in the map" claim does not apply to you
1 Does ORCA's node-pty ConPTY actually corrupt -CC control mode, as psmux's source warns for the SSH case? Run psmux -CC as an agentCmdOverrides target in ORCA and diff the %output stream against the same command run from a raw-pipe parent. One afternoon Corrupted → either apply 1a's remedy or drop -CC and use CLI verbs + polling. Clean → the SSH-scoping in the source was literal, the hazard does not generalise, and psmux's control mode is the best interface available to you
9 Is the no-PTY architecture sufficient for your actual sessions? claude -p --input-format stream-json + HTTP hooks + OTel covers everything except live screen fidelity and a human typing mid-run. This is the highest-leverage item in the document — a "no" moots most of sections 3 to 6. Instrument one week of real work: count sessions where you actually attached and typed mid-run vs. sessions you only observed. One week, passive Sufficient → stop evaluating multiplexers; build on pipes and hooks, and the entire PTY-ownership problem disappears. Not sufficient → you have a measured number for how often you need attach, which is the input to every remaining trade-off
3 Does Zellij's use_vt_path() gate hurt inside an ADE PTY? It selects the native-console path when neither TERM nor WT_SESSION is set. There is currently no pass/fail criterion — nobody knows what the failure would even look like. Launch Zellij under an ORCA pane, record the observable behaviour (keyboard responsive? input garbled? fine?), then re-test with TERM=xterm-256color forced. 1 hour Broken → one env var in the override command fixes it, and Zellij stays the most mature candidate. Fine → the most-cited hazard in three editions of this map was never real, and Zellij is the default choice
23 Do any candidate's verbs work with no console attached? Every "headless verbs" claim in section 4 is untested against a genuinely console-less caller — a service, a Scheduled Task, or a hook. This is the headless-first constraint stated as a test. Invoke zellij list-sessions, psmux ls, wezterm cli list and rmux ls from a Scheduled Task running with no console, capturing stdout/stderr to a file. 2 hours Work → the whole section-4 "headless verbs" column becomes trustworthy. Fail → those candidates are terminal tools, not middle layers, and the no-PTY architecture wins by default

Tier 2 — answers that pick between surviving candidates.

# Question Next step Cost If yes → / If no →
2 What does herdr's "Direct terminal attach | unsupported" mean, given the same page lists "Local persistent sessions" and "Windows Terminal / PowerShell app attach" as Supported (beta)? Demoted from top priority because the two tables together suggest it is the Unix fd-passing path, not the ADE-spawn path. Read src/api/, src/ipc.rs and the Windows-beta doc's surrounding sections; if ambiguous, file one question on the repo. 2 hours Unix-only mechanism → herdr is a live candidate with a 23k-star maintenance base. The ADE path → herdr is out on Windows until the beta lands
8 Can wezterm-mux-server be driven headlessly end to end on Windows — spawn, send-text, get-text, list, with a client attaching later? Install the nightly Windows zip, run wezterm-mux-server --daemonize, drive it with wezterm cli only. Half a day Yes → you get the only version-skew-tolerant codec in the map, at the price of tracking a nightly. No (attach needs the GUI) → the codec remains a design reference only
4 Does Electron/xterm.js answer XTWINOPS CSI 18 t? Read xterm.js windowOptions defaults; then empirically resize an ORCA pane and check the reported size inside psmux. 1 hour Answers → psmux's in-band sizing works and resize is solved. Does not → psmux silently runs at 120x30 forever, which is a silent-corruption-class bug, not a cosmetic one
10 Is ao pty-host extractable as a standalone package? Apache-2.0 makes vendoring clean and it is the closest architectural match found. Try go build ./backend/internal/adapters/runtime/conpty/... outside the AO tree and see what it drags in. 3 hours Clean → you have a working detached-ConPTY host to build on rather than write. Entangled → it is a design reference and you write your own
12 Do OpenCode's /pty endpoints work on native Windows with the shipped binary? opencode serve, then POST /pty + WS connect from PowerShell. 1 hour Work → a REST+WS attach surface exists today with a resumable cursor — but note it is L1+, not L2, and the wire is UTF-8 text. Fail → the protocol is still worth copying
18 Do any candidates run correctly nested inside a second ConPTY? Nobody has tested this, including oly — whose "nested agent tests" turned out not to exist. Run each under an ORCA pane with a resize, an alt-screen app, and a mouse-mode app. One day for all four Whichever survives is your candidate. This is the single largest untested area in the map
24 Can any candidate's prefix key be disabled entirely on a headless path, not merely rebound? Only docker documents remapping; every multiplexer's rebindability in 4.3 is an inference. For each of psmux, Zellij, rmux: find the config key, set it to none, and verify no chord is intercepted. 3 hours Yes → prefix collision with the ADE stops being a concern. No → you inherit a keybinding negotiation with every ADE you ever swap to, which is exactly what this project exists to avoid

Tier 3 — soundness and due diligence before adoption.

# Question Next step Cost If yes → / If no →
6 Is psmux's cross_session_server.rs path reachable in practice? It accepts the first connection with no handshake, registers it as a tee writer so it receives all pane output, and writes everything it reads into the ConPTY — full-duplex exfiltration plus injection on a loopback port. Trace every call site of cross_session_server, or attempt a PoC connect during a cross-session pane move. 3 hours Reachable → psmux is unsafe on a shared or multi-user machine until patched; report upstream. Unreachable → it is latent, and still a reason to pin and watch
7 Does quil's Windows ConPTY stack actually work? 25+ Windows source files, ubuntu-only CI, zero external issues ever filed. Run its test suite on Windows; add a windows-latest job locally and see what breaks. Half a day Works → you get the richest agent-facing surface in the map (18 MCP tools). Breaks → UNVALIDATED becomes REFUTED and the MCP surface is a design reference
13 Does oly's M4 named-pipe ACL gap still exist, or was it closed without updating the roadmap doc (untouched since 2026-03-12)? Diff src/ipc.rs across the full commit history for any ACL/SD-related change; or ask the maintainer. 1 hour Closed → oly's main disqualifier is gone. Still open → oly is unusable on any machine with another local account or a low-integrity process
22 Do the four small candidates have CVE/GHSA records? Only their own issue trackers were checked. One NVD/GHSA sweep before adoption sign-off. 1 hour Either way this is a gate you pass before shipping, not a discriminator
5 Is AttachConsole reliable in the exact topology that matters — a process spawned inside a headless ConPTY created by node-pty inside Electron? Write a 30-line helper that attaches to a claude.exe spawned by ORCA and calls GetConsoleProcessList. 2 hours Works → you gain a read-only out-of-band inspection path. Fails → 6.4's refutation is complete and late-attach is closed permanently
11 Does codex app-server --listen unix:// actually bind on Windows? The uds crate is cross-platform and the transport is registered unconditionally, but the daemon lifecycle manager is explicitly Unix-only. codex app-server --listen unix://%TEMP%\test.sock on Windows 11 and see. 30 minutes Binds → a second vendor-native no-PTY architecture is available. Fails → Codex stays stdio-only on Windows
14 Is oly's main-branch stall (no commits since 2026-07-01, though four side branches are active) a stable plateau or a departed maintainer? Watch for 4-6 weeks; check issue response latency. Passive Affects only whether oly is worth adopting versus reading
19 Should the middle layer's control plane carry ACP over a Windows named pipe? The spec formally permits it and agent CLIs are converging on it — but ACP has no Windows CI and no live-process attach primitive. Prototype the transport shim against the reference implementation and see whether it builds and passes on Windows. Two days Builds → the wrapper becomes protocol-native to the ecosystem it serves. Fails → use your own framing and revisit when ACP v2's terminal work lands
17 Has microsoft/terminal's in-process ConPTY redesign (doc/specs/#13000) shipped, partially shipped, or stalled? Cross-check against Windows Terminal changelogs and the ConPTY NuGet release notes. 1 hour Shipped → the reflow/resize desync in 6.1 may be fixable rather than permanent. Stalled → plan around it
15 Would kind or a Hyper-V-backend Podman work end to end on Windows without WSL? Architecturally plausible from both projects' docs; zero real-world confirmations located. One hands-on spike: podman machine init --provider hyperv then kind create cluster. Half a day Only matters if you want container isolation around the middle layer; it never replaces it (3.5)
21 Does any libghostty consumer have a working Windows build today? ghostel says yes for its own ConPTY module — is that libghostty on Windows, or ghostel routing around it? Read src/ConPtyProcess.zig and check what it imports. 1 hour libghostty → a fourth VT-parser option opens up on Windows. Routing around → avt and vt100-psmux remain the only two
20 Is there anything transferable in SCADA/industrial session tooling? Genuinely under-researched, not confirmed absent. One dedicated search round before writing the area off. 2 hours Low expected value; recorded so the gap is not mistaken for a finding
16 Does the Windows Hypervisor Platform feature work on Windows 11 Home? No Microsoft doc states an edition requirement either way. Only resolvable empirically or by a Microsoft support statement. Low priority — the tool that needs it (sbx) is closed-source and already refuted on constraint 2

9. Coverage Gaps

Stated plainly so nothing is mistaken for verified, and ordered by how much damage a wrong assumption does — most dangerous first. Inferences lead, because they are the items most likely to be read as facts.

1. Inference presented as reasoning, never observed. These are conclusions this document reached from verified premises. Each could be wrong without any of its premises being wrong.

  • That a foreign-SID console is not AttachConsole-able (the sandbox-runtime incompatibility, 3.5) — inferred from documented per-session console semantics, never tested.
  • That the session-0 boundary blocks AttachConsole (3.8, 6.3) — same class, same status. Earlier editions stated this as established fact in two places.
  • That the psmux -CC / ConPTY DCS hazard generalises beyond SSH (4.2). The source scopes it to "when running over SSH with a ConPTY console"; the generalisation to a node-pty ConPTY is this document's, and Q1 exists to test it.
  • That claude -p --output-format stream-json behaves identically on Windows and Linux (3.8) — inferred from unqualified flag documentation over ordinary pipes. This is the second finding's load-bearing cross-platform claim and nobody has run it on both platforms. Q23's neighbour in importance.
  • That asciinema/avt compiles on native Windows (3.7) — inferred from a two-crate dependency list with no platform code. Nobody has built it.
  • That eat inherits Emacs core's Windows-pty gap (3.8).
  • That Windows Job Objects are what kill sshd's children on disconnect (6.3) — asserted in one maintainer sentence, with zero code-search corroboration in openssh-portable.
  • That the multiplexer candidates' prefix keys are rebindable (4.3) — inferred from "they have config files". Only docker's --detach-keys is documented.

2. Not tested empirically anywhere. The map is a source-and-docs map. Almost nothing in it has been run.

  • Double-ConPTY nesting — by any candidate. Note this now includes oly, whose supposed nested-agent tests were found not to exist (3.1).
  • Any candidate actually spawned by ORCA.
  • Whether AttachConsole reaches an ORCA-spawned agent.
  • Whether any candidate's CLI verbs work with no console attached — the headless-first constraint has never been tested against a genuinely console-less caller (Q23).
  • What the observable symptom would be if Zellij takes the native-console input path inside an ADE PTY (Q3) — so Q3 currently has no pass/fail criterion.
  • The install commands in 3.1 were assembled from each project's own docs; none was executed on this machine.

3. Not re-verified in any pass. Believed, sourced once or not at all, never re-checked.

  • Zed's ACP constraint and Terminal Threads; OpenChamber's OpenCode-only scoping; coder/mux's own-agent-loop architecture.
  • Every closed-source entry (Kepler, Air, Warp/Oz, Factory, Devin, Jules, Cursor, Conductor, AIDEN) — nothing about these was verified at source, by definition.
  • The agent-vs-host classification for Cline/Kilo/opencode/Amp/Codebuff; automagik-genie and operator-oss; the entire sub-350-star ADE long tail.
  • rmux's "936 of ~1000 commits" single-author statistic; the wezterm 454ec0a7/9d834586 commit identifiers; the rmux breakaway error string's file and line; PowerSession-rs's missing-ResizePseudoConsole file and line; oly's extract_query_responses_no_client file; the LangChain 'response' discriminator; the pywinpty Discourse post's URL; the "actions/runner is near end-of-life" blog.
  • Nine citations in 7.1 record a method (gh api, README) rather than a retrievable object. Where the object was recoverable it has been filled in; where it was not, the row now names the repo and file at minimum. Known limitation: a handful still resolve only to a repo, not a line.

4. Never examined at all.

  • The xterm.js attach-addon raw-bytes-over-WebSocket convention, shared by ttyd, code-server and VS Code. This is the largest single hole in the protocol coverage, and the superlative earlier editions attached to it ("arguably the most widely deployed…") is itself unverified — it is now tagged [UNV] in 3.6.
  • chadbyte/claude-relay and myrialabs/ptykit ("PTY sessions over WebSocket — collaborative rooms, resilient client, Node & Bun", 1 star, pushed 2026-07-30).
  • ACP v2's terminal work; Codex's stdio-to-uds crate and its third remote_control transport.
  • Whether any candidate other than herdr documents Windows prefix-key behaviour (4.3).

5. Evidence that could not be retrieved. Both blocked cases are the same mechanism — an Anubis-style proof-of-work interstitial that returns HTTP 200 with a "Making sure you're not a bot!" body, so a naive status check passes them. Earlier editions described one as "access-gated" and the other as "an anti-scraper wall", which read as two different problems.

  • developer.valvesoftware.com — the Source RCON spec was not independently verifiable.
  • gitlab.freedesktop.org — the canonical OSC 133 spec; triangulated from three implementer docs instead.
  • The abduco/dvtm COSIN'18 slides did not extract.

6. Coverage bounds worth stating. The candidate pool came from GitHub topic and code search plus targeted follow-up. Anything not indexed by GitHub code search — self-hosted forges, unreleased internal tools, and non-English project descriptions beyond the handful encountered — is outside this map's reach by construction, not absent from the world.


10. How to narrow this yourself

This document deliberately does not pick a winner — the brief was to map the space, and section 1's verdict is that no candidate dominates. But "here is the whole space, plus 24 open questions" is not usable on its own. So: a decision scaffold keyed on your answers, not on a recommendation. Answer four questions and most of the map falls away.

Question A — Do you ever attach and type into a session mid-run, or do you only observe and steer through the agent's own interfaces?

  • Only observeThe no-PTY architecture dominates and most of section 3 is moot. claude -p --input-format stream-json over ordinary pipes, plus http hooks for the write path, plus OTel for liveness including blocked_on_user. No ConPTY, no nesting, no prefix keys, no resize propagation, no double parse — every hard problem in 4.1 and 4.2 disappears because you never own a PTY. Read 3.8, 5's last row, and stop. Q9 is how you answer this honestly rather than by intuition.
  • I attach and type → continue to B. You need a PTY owner, and the rest of this scaffold applies.

Question B — Do you need the same behaviour on Linux, or is Windows the only platform that has to work?

  • Linux parity matterspsmux is out (Windows-only; its CI has no Linux build, only two POSIX shell-script jobs). Also note oly ships a Windows-specific expected-output fixture, which is direct evidence its rendering diverges by platform. Survivors: Zellij, rmux, wezterm-mux-server, herdr.
  • Windows only → psmux comes back, and it has the strongest interface in the map (tmux -CC) — subject to Q1a and Q1.

Question C — Can you tolerate a preview or nightly channel, or do you need a pinned stable release?

  • Need a pinned stableherdr is out (Windows ships preview-channel only; stable v0.7.5 has no Windows asset) and wezterm-mux-server is out (no tagged release since 2024-02-03; the nightly tag is the only current Windows path). Survivors: Zellij (v0.44.3 msvc zip + MSI), rmux (winget/scoop/choco).
  • Nightly is fine → all four stay in.

Question D — Will you vendor and build source, or do you need something installable?

  • Installable onlyao pty-host is out (a Hidden: true subcommand, not a product) and OpenCode /pty is out as a middle layer (it is an API inside another product, and only L1+ — it dies with opencode serve).
  • Willing to vendor Go or Rustao pty-host opens up (Apache-2.0, closest architectural match found, detached ConPTY with scrollback replay and resize arbitration) and so does the WezTerm codec as a protocol to implement rather than a binary to run.

Where the branches land, in practice. Observe-only ends at the no-PTY architecture and needs nothing from this map's candidate list. Attach + Linux parity + stable pin ends at Zellij or rmux, and the tiebreak is maturity versus transport design: Zellij has 34.6k stars, an MSI, and the best-verified Windows claim in this map; rmux has the only identity-scoped namespace and the only three-layer Windows security model, against a single-author bus factor. Attach + Windows-only adds psmux, whose interface is the best in the map and whose platform breadth is the worst — decide it with Q1a. Willing to vendor adds ao pty-host as a starting point rather than a dependency.


11. If you decide to build it

Section 1's verdict is that nothing is packaged as the thing you want. The pieces of a build spec are, however, all present in this document — scattered, because they were found in different places. Collected here so the loop the verdict opens actually closes.

Where What it contributes
5.1 — the verb set The ten verbs, derived from the union of every candidate's surface. spawn, list, attach (with a resumable cursor), detach, send/send_keys, get_output/snapshot, resize, status, kill, wait. This is the interface
4.1 — the four Windows requirements Answer DA1 inside 3 s or eat multi-second stalls; do not set INHERIT_CURSOR unless you answer ESC[6n fast; load conpty.dll by absolute path or not at all; propagate resize explicitly because there is no SIGWINCH. This is what makes it work on the platform
3.7 — the PTY libraries, and the flags note under it portable-pty (Rust) or charmbracelet/x/conpty / aymanbagabas/go-pty (Go). Bundle the ConPTY NuGet redistributable — not because Microsoft blesses it but because fixes will not reach the in-box copy. And do not spend effort on creation flags: current ConPTY reads three, and the old passthrough bit now means something else
2.2's third pole — the raw live path Keep a VT model for snapshots, scrollback and resize bookkeeping, but ship raw child bytes on the live path. This is what makes the ADE's xterm.js the only parser in the chain. Carry bytes, never string — 2.2 and 4.2 both show what string-typed wires cost
3.6's adoptable contracts Do not invent framing. Either speak tmux -CC (decade-hardened, ≥5 implementers, and psmux/WezTerm prove it works on Windows), or copy WezTerm's varint codec, which is explicitly designed for version-skewed client/server pairs — i.e. it is the ADE-churn insulation, in wire form
2.3 + 5.1's closing note — the security model Identity-scoped namespace ({prefix}-{sid}-il-{integrity}-{label}, rmux's shape), a real pipe ACL, and FILE_FLAG_FIRST_PIPE_INSTANCE. Not loopback TCP: it is reachable by any local process including other users and low-integrity ones. psmux's cross-session hole is what that costs in practice
2.9 + 3.1's ao pty-host note — the persistence mechanism CREATE_NEW_PROCESS_GROUP | DETACHED_PROCESS so the host outlives the daemon, plus a registry file mapping {sessionId, ptyHostPid, pipePath} so orphans are findable after metadata loss
5.2 — the name for the shape Portal. A coding agent, a MUD engine and a Lisp REPL independently arrived at "the frontend is disposable, the long-running state lives behind a socket". That convergence is the strongest architectural signal in this map

And the honest caveat on building it. Every one of the ten candidates in section 1 got most of this right and still forfeited something. The two things that most often go wrong are not in the verb list: answering ConPTY's startup handshake (4.1 item 1, the single most load-bearing requirement here) and not putting a string type on the wire (2.2). Both are cheap to get right at the start and expensive to retrofit.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment