Quickshell logo
Windows 10 / 11 · x64 · Per-user install

SSH on Windows,
without the suite around it.

Quickshell draws the terminal grid on the GPU: one DrawInstanced of twenty-byte cells per frame, and no draw call at all on a frame where nothing changed. Sessions, keys, SFTP and port forwards are in the window. Nothing else is, and the list of what will never be added is published.

No X server, no bundled userlandNo telemetry, no accountNo background service
quickshell · prod-eu-1
Trying 10.42.7.19:22 …
prod-eu-1  host key known  ed25519 SHA256:9cE2r+wQ…kQ4  aes256-gcm@openssh.com
Last login: Thu Aug 27 09:14:02 2026 from 10.4.2.19

alex@prod-eu-1:~$ printf '%s\n' 'ascii │ 日本語 │ café │ 🚀 done'
ascii │ 日本語 │ café │ 🚀 done
alex@prod-eu-1:~$ systemctl is-active nginx postgresql
active
active
alex@prod-eu-1:~$ tail -f /var/log/nginx/access.log
10.4.2.19 - - [27/Aug/2026:09:14:38] "GET /health" 200 2
10.4.2.19 - - [27/Aug/2026:09:14:41] "POST /orders" 201 511
10.4.2.19 - - [27/Aug/2026:09:14:43] "GET /orders/8812" 502 166
 
The prompt is drawable under 400 ms from process creation. The second line is the one that catches most terminals out: Latin, CJK and emoji in one row, each taking the number of cells the remote shell believes it took.
.NET 10, WPF host with a child HWND per paneOne D3D11 device, one glyph atlas, one draw callZero allocations in the parse pathWidths generated from Unicode 17.0.0
Why leave the incumbent

The terminal is the product, not one tab of a toolkit

The clients this replaces grew an X server, a POSIX userland, a macro recorder and a plugin host around a terminal that still redraws the whole screen to move a cursor. Quickshell is the other trade: a small surface, drawn properly, with the cost of every part written down before it was built.

🖥

The grid is on the GPU

DirectWrite rasterises each glyph once into an atlas keyed on its subpixel offset, so a character keeps its weight in every column. The whole visible grid is then one DrawInstanced of twenty-byte cells, blended in linear light so text weighs the same on either background.

🌙

Nothing to draw while you read

Every mutation moves a generation the renderer compares against the one it last drew, and a scroll dirties one row rather than the screen. When the host stops writing, the window authorises no further frame: an idle Quickshell submits zero draw calls and holds no raised timer resolution.

Echo does not queue behind output

Output volume is the host's choice and echo latency is what you feel, so the two never share a queue. Under a hundred-megabyte cat the delay from key to glyph is the same as it is at rest, because frames are dropped under load and bytes never are.

🔑

The host you think you reached

Every connection is checked against known_hosts, written in the format OpenSSH reads back unchanged. A key that has changed raises a dialog with no default accept button: the old entry has to be removed deliberately, because a client that defaults to accept is a client whose encryption is decoration.

🗂

Your ssh_config is the session tree

Patterns, Include and ProxyJump chains are read as written, so a host you already defined for OpenSSH opens here without being defined a second time. MobaXterm and PuTTY sessions import, and every setting this client will not honour is named per session rather than dropped.

📐

The numbers are in the repository

Six figures, the machine they are measured on and the method that settles each one live in docs/PERFORMANCE.md, and a regression past any of them fails a build rather than reaching a release. A number without a machine is a mood.

Blocks A and B

A session that stays up, or says why it did not

A link that drops for ten seconds is ordinary on mobile and on a VPN, so it costs a reconnect and not the session. What cannot be recovered is said in a sentence naming the hop, the method and the remedy.

  • Reconnect keeps the tab: the scrollback, the pane layout and every forward come back on the session that dropped, and the client states which attempt restored it rather than reopening a blank tab beside the dead one.
  • Failures are named, not raised: a wrong port, a refused algorithm, a rejected key and an expired second factor are four messages with four remedies, because the error is the documentation a user reads at the moment something fails.
  • Keys, agents and tokens: ed25519 and RSA files, keyboard-interactive and two factors, and identities held only in the Windows agent or Pageant, which is the one route to a key on a hardware token that this client may never hold.
  • Passwords never rest in a managed string: what is stored is protected with DPAPI at user scope, so a copy of the store does not decrypt on another machine, and the settings surface says plainly that this does not stop an attacker already running as you.
  • Agent forwarding is a per-host decision: forwarding hands a remote machine the ability to authenticate as you everywhere, so it is answered per host rather than by a checkbox set once and forgotten.
  • Bastions are the ordinary path: a chain of jumps uses one code path at any depth, and the target's own host key is verified rather than the bastion's.
a link that dropped, and a key that changed
link lost  prod-eu-1  no data for 4.0 s
  reconnecting …  attempt 1 refused (network unreachable)
  reconnecting …  attempt 2 refused (network unreachable)
restored   prod-eu-1  on attempt 3, after 11.4 s
  scrollback kept, 18 240 rows · tab kept · 3 forwards restored
  forward -L 5432 → db-1:5432 up · -R 9000 up · SOCKS 1080 up

REFUSED  build-runner-4  the host key does not match the one on record
  stored  ed25519 SHA256:UkP0m1s+7Jd…Ax8   first seen 2026-03-04
  offered ed25519 SHA256:t7Qa9Rr2Zz…bN1   not accepted
  This is what a machine in the middle looks like. It is also what a rebuilt
  host looks like. Remove the stored entry deliberately to tell them apart.
 
The protocol itself is a dependency behind a seam, which is a stated non-goal: no SSH is implemented in this repository, and no library type reaches the terminal or the window.
Block C

Emulation that does not lie about the remote

A terminal is judged by the programs that already exist, so this one is measured against them rather than against tests its own author wrote.

  • esctest above ninety per cent, with every remaining failure named individually and accounted for by a reason or a task id. A suite written by whoever wrote the parser tests that person's reading of the specification, which is the part most likely to be wrong.
  • A parser that allocates nothing: Williams' table over fourteen states and all 256 bytes, emitting events without a single allocation in steady state, asserted over a full corpus replay that fails the build rather than printing a number somebody reads later.
  • Text that survives a split read: a stateful decoder and UAX #29 clusters, so a multi-byte character cut in half by the network arrives whole, and a combining mark lands on the cell it belongs to.
  • Widths the host agrees with: a table generated from Unicode 17.0.0, checked by printing mixed Latin, CJK and emoji and comparing the cursor column against what the remote shell believes.
  • Reflow that is reversible: narrowing and widening the window restores the original rows exactly, proven by property tests over random buffers and width sequences rather than by dragging a window.
  • Pixels checked on five environments: the golden-image suite runs on NVIDIA, AMD, Intel integrated, WARP and inside an RDP session, because a driver bug is by definition the thing the machine that wrote the code cannot see.
protocol trace · what a host is answered, and what it is not
quickshell · protocol trace · session prod-eu-1 · secrets redacted

→ host  CSI c                        "what are you"
← us    ESC [ ? 65 ; 1 ; 9 ; 15 ; 22 c   a VT500-series terminal, and here is what it has

→ host  OSC 52 ; c ; ?  BEL          "read me the clipboard"
← us    (nothing)                    refused: write-only when on, off by default

→ host  OSC 0 ; deploy: staging  BEL set the window title
← us    (nothing)                    title set, never reported back

→ you   click at row 12, column 240
← us    X10 encoding cannot name column 240; asked the host for SGR (1006)
        the alternative is naming a different cell and calling it a click
 
What a hostile host is refused is decided too: OSC 52 cannot read your clipboard, the title is set and never reported back, and a paste is bracketed so the host cannot run it as though you had typed it.
Blocks E and F

Files and ports, on the session you already opened

A transfer and a forward are channels on a connection that exists. Opening a second one would cost you a second password, a second second-factor and a second entry in someone's audit log, for no gain.

SFTP as a thing a person operates

  • Browse without running a command: a pane on the same session, listing fifty thousand entries with the first screen visible immediately rather than when the last row arrives.
  • A queue that resumes: progress, cancel, and a restart from the offset it reached, refused when the partial cannot be shown to be a prefix of the source rather than continued into a file unlike its source.
  • Folders and collisions are policy: recursion is explicit and a name that already exists asks, because this is where a transfer tool quietly destroys data.
  • Drag from Explorer: the interaction people try first, onto a session or onto a directory in the browser.
  • Compare a local and a remote directory: the deploy-and-check loop, with what differs listed instead of eyeballed.
  • SCP for the appliance that offers nothing else: kept as a fallback, never as the primary path.

A forward is a lifecycle, not a checkbox

  • Local, remote and SOCKS: a database client reaching a machine it has no route to, a service on your laptop reachable from the host, and one proxy covering a whole remote network.
  • Restored with the session: every forward comes back after a reconnect, or the client says which one did not and why.
  • Loopback unless you say otherwise: no forward binds beyond 127.0.0.1 without an explicit choice, on a machine with any number of interfaces.
  • No listener outlives its session: closing a session under load leaves the process holding no listening socket, checked from outside the client.
  • A list of what is running: a forward is invisible by nature, so the client shows its own listeners instead of sending you to netstat to understand it.
Block G

The clean interface, defended

A first run on a clean profile shows a title bar and a terminal. Everything else is reachable and nothing else is resident on the screen, which is a decision the defaults make on your behalf and the settings let you undo.

  • Tabs and panes, one device between them: sixteen panes share one D3D11 device, one glyph atlas and one set of shaders, so atlas memory at sixteen panes differs from one pane only by the instance buffers.
  • Everything is in the palette: the action list is generated from the actions themselves and a test asserts the palette enumerates all of it, so an action that exists and cannot be found fails a build.
  • Broadcast to the panes you choose: the same command on eight hosts, typed once, because mistyping the eighth is how fleet work fails today.
  • Schemes in the formats people already share: iTerm2 and Windows Terminal colour schemes are read as published rather than retyped as twenty colours.
  • A screen reader reads the grid: a GPU surface is opaque to assistive technology by construction, so the text is published deliberately and the cursor is followed as a prompt is typed into.
  • Settings are a file you can move: versioned, hand-editable, and round-tripped without being reformatted, reordered or losing a key it did not recognise.
The budget

Six figures, one machine, and the method for each

These were written before the code they bind, and each is changed only by a commit that argues for the change. A regression past any of them fails a build.

Input to photon
< 8.3 ms one refresh at 120 Hz

A keystroke echoed by a local shell, from the key going down to the glyph being on the glass. Settled by a high-speed capture of key and screen in one frame, or an instrumented path that timestamps the input event and the present. Never by feel.

Parse throughput
≥ 400 MB/s sustained

Mixed text and escape sequences, end to end, on a headless harness with no renderer attached. Parsing must never be the reason output is slow, so it is measured where nothing else can be blamed for it.

Steady-state frame
< 2 ms 200 × 50 grid

One filled grid redrawn continuously, GPU and CPU time, in a steady state rather than on the first frame. Leaving the frame budget almost entirely unspent for one pane is what keeps several panes affordable.

Idle cost
0 draw calls

A window nobody is typing into: zero draw calls submitted over an idle interval, no measurable occupancy on any core, and no raised system timer resolution held by the process. This is the figure a laptop user feels as battery life, so small is not a pass here.

Cold start
< 400 ms to a live prompt

Process creation to an interactive local shell, which means a prompt that accepts a keystroke and not a window that has appeared. Reported for a cold file cache, with the warm figure beside it where a cold one cannot be arranged.

Resident memory
< 120 MB one session, default scrollback

The private working set after the session has settled. It is also held flat across a seventy-two-hour soak, because a number that passes on minute one and drifts is a leak that passed.

The reference machine is named in docs/PERFORMANCE.md: an i7-14700, an RTX 4060, a 3840 × 2160 display at 60 Hz, on Windows 11 Pro. A second machine may be measured, and then the machine is named beside the number, because two machines quoted as one is how a regression becomes a rounding difference. A figure quoted without the run that produced it is not a measurement.

Leaving MobaXterm

Your sessions come with you, and what does not is named

The session tree is the artefact you built over years, and recreating it by hand is the real cost of changing client. It is also the reason most people never get past the first evening with a new one.

1

Point it at what you have

An OpenSSH config, a MobaXterm session file or a PuTTY registry export. Patterns, includes and jump chains are read as written rather than flattened into a list.

2

Read what was skipped

Every setting this client will not honour is reported per session, by name. An X11 setting or a macro is refused out loud, because a silent drop is a defect report three weeks later.

3

Keep both for a while

Nothing is rewritten in place: the files the incumbent reads are left as they are, and the known_hosts this client writes is one OpenSSH reads back unchanged.

Stated, not accidental

What this will never grow

The full list is in the roadmap and it is not a wish list of things not yet reached. A client is lean because of what it refuses, and refusing it in writing is what makes the refusal hold.

No X11 server or forwarding

The single largest thing the incumbent carries, and the reason its install is measured in gigabytes.

No bundled POSIX userland

No Cygwin, no BusyBox. Windows has a package manager and a WSL, and neither of them is this client's job.

No RDP, VNC, Telnet, serial or FTP

One protocol, done properly. A second protocol is a second product wearing the same window.

No web runtime

No Electron, no WebView2, no JavaScript terminal. The frame budget on this page is the whole argument.

No second graphics backend

D3D11 and nothing beside it. WARP covers the machine with no usable GPU, and a CPU-rasterised grid is refused outright.

No feature carried over because MobaXterm has it

Parity is not the goal, and a macro recorder, a scripting engine and a plugin host are all refused in the MVP.

Install it

Per-user, signed, and no administrator prompt

Windows 10 or 11, x64. It installs under %LOCALAPPDATA% and asks for nothing else, which is what reaches a managed laptop with SmartScreen on.

No service, no scheduled taskNo account, no telemetrySettings in one file you can copy

The asset carries its version in its name and SHA256SUMS.txt beside it is what a download is checked against. Building from source needs the .NET 10 SDK and Windows, and it is how a contributor works on this rather than how anybody has to run it.