# Windows Where noctty differs from upstream Ghostty's macOS and Linux documentation. For a row-by-row mapping against upstream docs, see [windows-capability-matrix.md](windows-capability-matrix.md). For VT protocol coverage validated on the Win32 runtime, see [windows-vt-conformance.md](windows-vt-conformance.md). ## Supported systems - Windows 10 version 1809 (build 17763) or newer, and Windows 11, on x64 and ARM64. 1809 is a hard floor: `CreatePseudoConsole` is a static `kernel32` import, so an earlier build refuses to start the process rather than degrading. - Native Win32 application runtime. - OpenGL 4.3 or newer through WGL for the default renderer; the opt-in D3D11 beta needs feature level 11.0 and can use WARP software rendering. - `libghostty-vt` stays portable as a library. This repository does not ship macOS, Linux, GTK, Wayland, or X11 app runtimes. Windows build targets use at least `win10_rs5` (1809), including explicit `-Dtarget` builds. This selects Zig's POSIX `Dir.rename` semantics when the filesystem supports them, so renaming over a file another process holds open works when that opener shares delete access. A holder without `FILE_SHARE_DELETE`, and filesystems that need Zig's legacy rename fallback, still reject the replace. `noctty.exe` carries the build version in its PE version resource. Release builds use `-Dversion-string=major.minor.patch`; the fixed numeric version is `major.minor.patch.0`, matching the portable updater's downgrade guard and Inno Setup. Without that option, a development build uses the base version from `VERSION` or `build.zig.zon` for the numeric fields and retains the `-dev` suffix and build metadata in its version strings. Packaging requires the staged executable's version string and numeric fields to match `-Version`. ## D3D11 renderer beta OpenGL remains the default. A normal Windows build includes both renderers. To opt in, add this to your config and open a new window, tab, or split: ```ini renderer = d3d11 ``` D3D11 first tries hardware, then Microsoft's WARP software renderer if a capable hardware device cannot be created. An unavailable D3D11 runtime or failed software initialization falls back to OpenGL. Selection and fallback reasons appear in the host banner; the inspector shows the active backend and `+version` reports whether the build includes the beta. `renderer = d3d11-warp` selects WARP explicitly for driver diagnostics; `renderer = opengl` restores the default for newly created surfaces. Changing the backend preference does not interrupt an existing terminal session. Custom shaders, background images, Kitty graphics, and generated image overlays use OpenGL in this beta. Configured shaders/images select OpenGL at startup; enabling them on reload or receiving terminal image content switches that surface to OpenGL while preserving its terminal and search state. The surface stays on OpenGL for its lifetime after fallback. GLSL support still depends on a build with custom shader support, as it does for the default renderer. OpenGL fallback needs a working OpenGL 4.3 driver. If it cannot start, a healthy D3D11 surface continues drawing text, ignores unsupported images and effects, and shows a notice. A later config reload can retry the feature. Unavailable devices retry fallback at five-second intervals instead of spinning. Device removal/reset triggers resource reconstruction and re-uploads retained atlas/buffer data. Recovery permits one hardware retry per sixty-second window, then WARP; a later loss after that window can re-probe hardware. Failed WARP reconstruction falls back to OpenGL. Hardware resource or presentation errors also get a bounded WARP attempt. Presentation follows the existing Win32 UI thread paint and power-saver policy. Occluded frames keep their render target and probe presentation at most four times per second while visible; hidden tabs and minimized windows suspend the renderer's draw timer. Known limits: WARP can consume substantially more CPU under heavy output. Its blend quantization can differ from hardware/OpenGL at glyph edges by up to four channel levels in the default corrected-linear mode. D3D11 does not add per-pixel translucency or custom HLSL effects. Its flip-model swap chain is composed through DirectComposition beneath the terminal HWND's children, with the existing host/sibling clipping. Framebuffer alpha remains ignored; the host still owns whole-window opacity. Overlay ordering, opacity, and visible OpenGL handoff require the maintainer desktop check below. GPU loss injection in the automated harness tests the recovery path, rather than a physical GPU reset. Hidden-desktop readback proves rendered pixels, and does not prove DWM composition, scanout, unoccluded frame latency, monitor hotplug, or RDP behavior. Developers can build with `-Dd3d11=false` to omit the beta backend. GPU parity and recovery tests require `-Drenderer-test-tools=true`; those readback and fault-injection controls are absent from normal builds. Run `test/windows/renderer/Invoke-RendererParity.ps1` against the resulting executable; it copies a portable run, redirects app data, and uses a hidden desktop. On ARM64/hosts without a suitable OpenGL driver, `-WarpOnly` runs the software-rendering lifecycle smoke, including failed GL image/effect fallback, without claiming cross-backend parity. For a visible check on your own desktop, build a normal ReleaseFast binary and run: ```powershell pwsh -NoProfile -File test/windows/renderer/Invoke-VisibleD3D11Check.ps1 -Binary .\zig-out\bin\noctty.exe ``` The script stages a separate portable copy, redirects app data, records pass/fail for the palette, paste confirmation, scrollbar, quick-select, opacity 0.8, and a Kitty-triggered OpenGL handoff. Screenshots are optional and capture displayed pixels. `-PrepareOnly` validates staging without launching a window. It changes only the staged config and never sets the clipboard or takes foreground. An automated readback alone is not a passing visible-desktop check. ## Install modes Each release publishes signed Windows artifacts for x64 and ARM64: - `noctty--windows--setup.exe`: installed app with Start menu shortcuts and app identity metadata. - `noctty--windows--portable.zip`: portable use without an installer. - `noctty--windows--portable.manifest.ps1`: signed hashes for the exact portable payload. - `SHA256SUMS-windows-.txt`: architecture-specific checksums. The installer, portable manifest, and binaries inside the portable ZIP are Authenticode-signed with a self-signed certificate. The ZIP container is checksummed and bound to the canonical release workflow by GitHub build provenance. The legacy `SHA256SUMS.txt` file remains an x64 auto-update compatibility alias. A Chocolatey package (`noctty`) is built by the release workflow but has not been published yet, so `choco install noctty` does not work today. It will after the first push clears chocolatey.org moderation. [getting-started.md](getting-started.md) covers download and install, including the SmartScreen warning. [verify-release.md](verify-release.md) covers checksum, provenance, and signature checks. ## Paths Runtime state lives under `%LOCALAPPDATA%\noctty\` by default. The shared XDG helpers still honor `XDG_CONFIG_HOME`, `XDG_STATE_HOME`, and `XDG_CACHE_HOME` when set on Windows; `%LOCALAPPDATA%` is the fallback in the normal packaged app environment. | Path | Purpose | | ------------------------------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `%LOCALAPPDATA%\noctty\config.ghostty` | User config, written on first launch. | | `%LOCALAPPDATA%\noctty\session-state.json` | Window, tab, split, profile, cwd, and title restore state, saved only by the noctty process that owns it (see Session restore and recovery), and never in safe mode, when elevated through UAC, or with `window-save-state = never`. | | `%LOCALAPPDATA%\noctty\layouts\` | Named layouts, stored as one-window session-state JSON documents. | | `%LOCALAPPDATA%\noctty\crash\` | Local crash dumps. Nothing here is uploaded automatically. | | `%LOCALAPPDATA%\noctty\shell-integration\` | Installed shell-integration payloads and manual fallbacks. | [PRIVACY.md](../PRIVACY.md) describes the other files and registry entries noctty writes, including the jump list, command palette history, and update and startup records, and how to remove them. The portable ZIP carries the bundled resources next to the executable. Do not move `noctty.exe` alone out of the extracted tree; it needs the packaged `share` resources for themes, terminfo, shell integration, and other data. ### Portable mode Place a regular file named `noctty.portable`, `portable.txt`, or `config.ghostty` beside the executable to opt into portable mode; a directory with one of those names is not a marker. A freshly extracted ZIP is not portable by default, because it ships `config-template.ghostty`, which is not a marker. An empty `noctty.portable` is enough. A lone `config.ghostty` is ignored when the setup installer's machine-wide uninstall entry records the executable's directory as its `InstallLocation`, so a config file dropped beside an installed `noctty.exe` does not move its data. `noctty.portable` and `portable.txt` still count in an installed directory. In portable mode, config, state, and cache live beside the executable (`config.ghostty`, `update-state.json`, session state, crash dumps, caches). That location takes precedence over `XDG_CONFIG_HOME`, `XDG_STATE_HOME`, `XDG_CACHE_HOME`, and `%LOCALAPPDATA%`, so the folder moves as one unit without leaving runtime data on the host. If `%LOCALAPPDATA%\noctty` already holds data, startup warns that portable mode is using the adjacent data instead; nothing is migrated. ## Shells For commands launched through `cmd.exe /C`, noctty sets `NoDefaultCurrentDirectoryInExePath` in that wrapper's environment when it is absent. This disables cmd's implicit executable search in the pane's current directory, so a planted `pwsh.cmd` cannot override a `pwsh` on PATH. The guard is inherited by descendants: clearing it before the target would restore the unsafe search. Existing values are preserved, and direct launches receive no new guard. Command text, `PATH`, `PATHEXT`, variable expansion, builtins and `||` fallback retain native cmd behavior, including PATH directories containing literal `%` or `!`. Explicit paths such as `.\tool`, and explicitly configured relative PATH entries such as `.`, still select programs relative to the pane. The profile picker detects: - PowerShell 7+ (`pwsh.exe`) - Windows PowerShell (`powershell.exe`) - Command Prompt (`cmd.exe`) - Git Bash - WSL distributions, when `wsl.exe` is installed and responsive Shell integration is automatic for PowerShell and for directly launched Unix-like shells such as Git Bash. WSL sessions manage their own integration inside the distribution. PowerShell integration emits OSC 7 working-directory updates and OSC 133 prompt markers. noctty injects no shell integration into `wsl.exe` profiles, so a WSL shell reports no working directory until its startup file does, so a new tab or split from it starts in the profile's own directory (the home folder for the built-in WSL profiles, which launch with `--cd ~`), not where you were. To report the directory, add the recipe Windows Terminal documents to `~/.bashrc` or `~/.zshrc` in the distribution: ```bash # bash PROMPT_COMMAND=${PROMPT_COMMAND:+$PROMPT_COMMAND; }'printf "\e]9;9;%s\e\\" "$(wslpath -w "$PWD")"' # zsh keep_current_path() { printf "\e]9;9;%s\e\\" "$(wslpath -w "$PWD")" } precmd_functions+=(keep_current_path) ``` `wslpath -w` prints `\\wsl.localhost\\home\me`, which noctty takes as that distribution's `/home/me` (see the [capability matrix](windows-capability-matrix.md#shell-integration)). The parser accepts it (it has a unit test for exactly this form), but the snippet was not run end to end through `wsl.exe` for this page. It reports the directory only; OSC 133 prompt and command marks still need a shell integration of their own. For `cmd.exe`, noctty wraps the inherited `PROMPT` (or cmd's `$P$G` default) so the shell emits OSC 133 A/B prompt marks and an OSC 9;9 working-directory report; a `PROMPT` set through `env = PROMPT=...` is applied afterward and replaces the wrapper. When the shipped `noctty-shell-integration.lua` is present, noctty prepends its Lua directory to `CLINK_PATH`, preserving existing entries. That only makes the script discoverable once Clink is already active through autorun, `clink inject`, or an explicit Clink launch; noctty never injects or launches Clink. When loaded, the script adds OSC 133 C/D command marks and the exit code before the next prompt. It also wraps the final normal prompt after Clink filters, so themes which replace the prompt and stop the filter chain keep prompt and cwd marks. Existing OSC 133 A marks are respected, and right and transient prompts are left alone. This final-prompt hook uses Clink's guarded `_internal._filter_prompt` seam (verified with Clink 1.9.34); versions without it retain the PROMPT fallback and C/D hooks. Exit codes are truthful only while Clink's `cmd.get_errorlevel` setting is enabled (its default). Without it, prompt navigation and cwd reporting still work, but there are no command-finish marks or exit codes. Caveats: on a UNC working directory `$P` yields `\\server\share\dir`, which noctty reports unchanged, but cmd.exe itself cannot use a UNC cwd (it warns and falls back), so a new cmd tab or split will generally not land on the share. A `PROMPT` ending in an odd number of `$` gets a `$S` (one space) inserted before the closing mark so cmd does not pair the dangling `$` with the mark's `$E`. `PROMPT` is session-wide, so `echo on` batch runs and `cmd /c` children can echo the wrapped value and emit spurious OSC 133 marks. With OSC 133 shell integration active, the command palette also offers: - `jump_to_prompt:-1` / `jump_to_prompt:1` (`Ctrl+Shift+PageUp` / `Ctrl+Shift+PageDown`): previous / next prompt. - `copy_last_command_output` (no default keybind): copy the last completed command's output. Needs OSC 133;C and OSC 133;D boundaries. - `insert_last_command` (no default keybind): put the last recoverable single-line command back on the prompt without submitting it. Needs OSC 133;B and OSC 133;C boundaries; noctty never reads or modifies the shell's line editor. It refuses on the alternate screen, while a command is running, and when no OSC 133;B input mark is outstanding (pressing Enter, or pasting a line terminator, consumes the mark until the shell draws the next prompt, which matters for the A/B-only cmd.exe integration), and it rejects text that is empty, multi-line, over 4096 bytes, or carries control or invisible formatting characters. It does not press Enter, by design: OSC 133 marks carry no provenance, so any program that can print (a file you `type`, a log you tail, a host you SSH into) can forge a complete A/B/C/D lifecycle around text of its choosing and have it retained as "the last command". The text is inserted the way a clipboard paste is (bracketed when the terminal asked for it); you read what landed on the prompt and submit it yourself. After a resize or repaint, ConPTY can redraw the whole screen and retag previously marked cells as output, so a recovered command or output region may occasionally be unavailable or truncated afterward. `utf8-console` (`auto`, `always`, `never`) controls UTF-8 setup for bare interactive `cmd.exe` launches and for Windows PowerShell 5.1 and PowerShell 7 sessions. `auto` (the default) enables UTF-8 unless the machine ANSI or OEM code page is a legacy CJK page (932, 936, 949, 950, or 1361); `always` ignores that guard; `never` leaves the encoding alone. The cmd preamble applies only to an interactive launch with no `/c`, `/r`, or `/k` payload: option switches such as `/d`, `/q`, and `/v:on` are kept in order ahead of it, and it is a no-op when the console already uses code page 65001. The PowerShell half runs inside the injected shell-integration script, so it does nothing with `shell-integration = none` or a command line that injection declines (`-Command`, `-File`, `-EncodedCommand`, `-NonInteractive`). WSL and Git Bash are unaffected. WSL appears in the picker but never becomes the default shell implicitly, because `wsl.exe --status` can report a healthy installation even when launching a session would fail. To make it the default: ```ini command = wsl.exe ``` A session that `wsl.exe` starts sees the same terminal as one that `cmd.exe` does. `wsl.exe` passes a Linux process only the Windows variables that `WSLENV` lists, so noctty adds `COLORTERM`, `TERM_PROGRAM` and `TERM_PROGRAM_VERSION` to the `WSLENV` of a WSL launch, each with the `/u` flag so that a Windows program started from inside WSL does not get the Linux values back. It adds `TERM` too when the distribution has a terminfo entry for noctty's `term`, which noctty asks the distribution itself with `infocmp`; the answer for an explicitly selected distribution is remembered until noctty exits, so install the entry first, then start noctty again. An implicit distribution is probed for each launch, so changing WSL's default while noctty runs cannot reuse the previous distribution's answer. To give a distribution the entry, compile `share/terminfo/ghostty.terminfo` from the noctty folder with `tic -x` inside it. Without the entry `TERM` stays `xterm-256color`, the value `wsl.exe` sets, and the other three are still forwarded. The first WSL tab of a stopped distribution waits for it to start, up to 15 seconds, before its shell appears; if it has not answered by then, that run keeps `xterm-256color`. Entries already in `WSLENV`, their flags and their order are kept, and a `WSLENV` set with `env` in the configuration is the one extended. A `TERM` set with `env` is forwarded as it is. A WSL shell started from inside cmd or PowerShell is not changed: only a launch of `wsl.exe` by noctty is. An existing `/w` entry stays Windows-only; noctty does not override that choice. Automatic `TERM` probing uses the launch environment and working directory, including `--cd`, and caches answers separately for different environments or directories for explicit distribution selections. The optional probe resolves only a local `.exe`; if resolution would need a network/device path, a mapped network drive, or reparse traversal (including directory symlinks and junctions), it keeps the fallback. For shell-form commands, automatic `TERM` is limited to a single `wsl.exe` command without variable expansion, redirection, command operators, or ambiguous quote/backslash sequences. Complex shell commands and a bare `wsl` without `.exe` still receive the identity variables, but keep WSL's default terminal type. Shell-form launches also keep the fallback when a CMD AutoRun hook is present or cannot be checked: the hook may change the environment before WSL starts, and noctty preserves its normal execution rather than running it twice. The probe uses the exact executable without `PATHEXT` script fallback and honors CMD's `NoDefaultCurrentDirectoryInExePath` choice. Per-tab surface IDs do not split the cache unless the user lists them in `WSLENV`. Direct launches keep the fallback if their prepared executable is still a bare or relative name: Windows process search can select a different binary from the probe's shell search. `ssh.exe` is different. It sends the PTY's `TERM` by itself, and the other variables travel only through the opt-in `ssh-env` shell integration feature (`SendEnv`, which the server must accept), with `ssh-terminfo` choosing the `TERM` for hosts that have the entry. ## SSH hosts noctty reads `%USERPROFILE%\.ssh\config` read-only whenever Windows profiles are refreshed. Each concrete alias on a `Host` line appears as `SSH: ` in the profile picker and `Connect to ` in the universal palette. Opening either runs the system `ssh.exe` with the alias as its only argument, in a new tab in your home directory. Set `ssh-config-hosts = false` to remove these entries. - Only `Host` lines are read. A line may carry several aliases. Wildcard or negated patterns, and aliases that are not plain printable ASCII, are skipped. - Every other keyword (`HostName`, `User`, `Port`, `ProxyCommand`, and the rest) is left to `ssh`, which reads the same file. `Match` blocks are ignored, including their `Include` lines. - `Include` files are read one level deep, up to 16 files. Relative paths start at the `.ssh` directory; absolute and `~/` paths are accepted. A leading `"` groups one path containing spaces, as in OpenSSH. A backslash is a path separator, not an escape. - An include is skipped before it is opened when it is globbed, UNC (`\\server\share`), a device path (`\\?\`, `\\.\`), or a mapped network drive letter. The drive check uses `GetDriveTypeW`, which answers from the local mount table without contacting the server. It misses a directory junction or `subst` alias with a remote target, but the bounded worker still keeps that open off the UI thread. Skipped glob Includes emit a warning once, sharing the warning budget for unreadable SSH configuration files. - The root config and `Include` opens run off the UI thread under one shared 50 ms refresh budget. A slow or blocked path is skipped; a later profile refresh can retry it. Activating an entry runs `ssh `, so whatever the alias resolves to in your SSH configuration takes effect, including any `ProxyCommand` or `LocalCommand`, which run locally. noctty adds no execution of its own; it turns the file into a one-click surface. SSH entries run without shell integration, which is injected into a local shell and cannot apply to a remote session. Session restore does not reopen them: an SSH pane comes back as the default shell rather than dialing out unattended. SSH entries sort after the detected shells; `NOCTTY_WIN32_PROFILE_ORDER` covers shell profiles only. There is no bundled SSH client, secrets vault, or fleet/connection management. ## App identity Installed builds create Start menu shortcuts with noctty's AppUserModelID, which Windows uses for taskbar grouping and Action Center toast activation. Portable builds have no installer-created shortcuts, so taskbar and notification identity may be less stable. If Windows shows a stale icon after upgrading or switching between installed and portable builds, restart Explorer or clear the icon cache before assuming the build is broken. The icon in a toast's header comes from the `noctty.ico` next to `noctty.exe`; the installer, the portable ZIP, and `zig build` all stage it there. Windows does not read the icon out of the executable for toasts, and it caches the first icon it resolves per AppUserModelID, so a fix to the registered icon is not picked up by the running session. A reboot clears that cache; whether signing out alone does has not been measured. A copy of `noctty.exe` without its sibling `noctty.ico` leaves the registered icon untouched rather than replacing it with one Windows cannot show. ## Default terminal > [!WARNING] > Default-terminal registration is experimental. A delegated console > application is now adopted into a visible noctty window, but noctty cannot > appear in the Windows Settings picker and ships no console half of its own. > Register only on a machine you can restore, and unregister after testing. ```powershell noctty +register-default-terminal noctty +unregister-default-terminal ``` Both commands are idempotent. Registration writes only per-user (`HKCU`) selection and COM proxy state; the installer registers noctty's owned COM local-server and proxy classes machine-wide but does not change any user's default terminal. Portable builds can register the absolute paths of the current `noctty.exe` and its sibling `noctty-terminal-handoff-proxy.dll`; moving either file afterward requires registering again. Registration refuses to proceed when the proxy DLL is missing, or when `DelegationConsole` is missing, empty, or set to the inbox console host. Deleting a copy that registered itself leaves `DelegationTerminal` pointing at files that are gone, and every console then opens in OpenConsole's own window. The installer's uninstaller runs `+unregister-default-terminal` first. So does the Scoop manifest's `pre_uninstall`, on `scoop uninstall` only (never `scoop update`), and only when the registration points into that app's Scoop folder and no process is running from it. Otherwise it skips and leaves the registration in place, as it does when the command fails to start or exits nonzero, which it reports without failing the uninstall. To be sure with Scoop, close noctty and run `noctty +unregister-default-terminal` before `scoop uninstall`. For a portable ZIP, run it yourself before deleting the folder. The three shared Microsoft `ITerminalHandoff` Interface mappings are snapshotted before noctty changes them. Unregistration restores each prior raw value only while that mapping still points to noctty's proxy, so a newer terminal's registration is preserved. A mapping that did not exist before registration is removed again, together with the `Interface\{IID}` key registration created for it, but only when nothing else has been added to those keys. It restores the `DelegationTerminal` value saved at registration only when noctty is still selected, then removes noctty's per-user COM registration. It never overwrites a default-terminal choice made after noctty was registered. Registration state written by an earlier build did not record which keys it created, so unregistering it leaves those keys in place, as before; registering again does not add the record while noctty's mapping is still in place. A handoff never reaches the noctty you already have open. Windows activates noctty through COM by starting `noctty.exe -Embedding`, and only that process registers the handoff class object; a normally started noctty does not. Each handoff is therefore served by a separate noctty process (a later handoff reuses it while it is running, and it exits once it has no windows and no pending handoff), and it opens its own top-level window for the adopted console, not a tab in your existing one. That process neither forwards to nor serves single-instance IPC, so `+list-windows` and `+perform-action` cannot see its windows, and it has no jump list or global hotkeys. It also restores no session and opens no initial window of its own. An adopted console starts with the title and show state its client asked for. The window title is the startup title (usually the program's path) until the console sets another one, so cmd's `title`, PowerShell and full-screen programs name the window as they do anywhere else. When the client set `STARTF_USESHOWWINDOW`, `start /min` and `Start-Process -WindowStyle Minimized` open the window minimized without taking focus, `SW_SHOWNOACTIVATE` opens it without taking focus, and a maximized start opens it maximized. `powershell.exe -WindowStyle Hidden` hides its own console window after it starts. For an adopted console, noctty follows OpenConsole's show/hide notifications as Windows Terminal does: hide minimizes the window, and show restores a minimized window. A title set in the configuration stays in force. A client that asked for a hidden console gets a minimized window that takes no focus: Windows hands every such console to the terminal, and a window nobody can bring back would be worse. Only `CREATE_NO_WINDOW` keeps a console from being delegated. noctty implements the terminal half of Windows console delegation and needs a Windows Terminal OpenConsole installation to remain selected as the console half. It does not verify that the selected console CLSID still resolves to an installed application, so a value left behind by an uninstalled Windows Terminal is accepted and console launches degrade until a working console half is selected again. The handoff supports `ITerminalHandoff3` only, as used by Windows Terminal 1.24 or newer. Older OpenConsole versions that request v1 or v2 are not supported; Windows falls back to a normal console window. noctty is an unpackaged application, so it does not appear in the Windows Settings default-terminal picker; the registration command writes the per-user registry values directly. Candidate enumeration accepts a console and terminal pair only when both come from the same package, and selecting anything in the picker overwrites noctty's pair. Package identity would be required to fix this. For a handoff trace, set `NOCTTY_HANDOFF_TRACE=1` before launching the delegated console application. noctty appends `event=` milestone lines and `reason=`/HRESULT rejection lines to `%LOCALAPPDATA%\noctty\handoff.log`, which distinguishes "never activated" from "adopted, then lost on the way to a window". The opt-in file is truncated before an append would exceed 1 MiB. ### Security properties of registration Registration creates a same-user activation surface, which is inherent to Windows console delegation. Once registered, any local process running as the same user at the same integrity level can activate noctty's handoff class directly and call `EstablishPtyHandoff` with pipes and a window title it controls. The result is a real noctty window rendering that process's content, with the user's keystrokes flowing back to it: a phishing shape, for example a window titled `Administrator: Windows PowerShell`. No privilege boundary is crossed; such a process already runs as the user and could impersonate a terminal in other ways. The handoff rejects callers whose integrity level does not match noctty's own. That rejection must stay: it stops an elevated console session from being rendered inside a medium-integrity terminal that the user's other processes can drive. The per-user `Interface\{IID}\ProxyStubClsid32` mappings are required for normal console-delegation clients, but they are configuration, not an access-control boundary. A same-user medium-integrity process can create an equivalent HKCU mapping to the installer-published proxy class and activate the handoff class directly, even before the registration command has run. `EstablishPtyHandoff` returns the two OpenConsole-side pipe ends as `[out] system_handle(sh_pipe)` parameters, and that marshaling transfers ownership: once the method returns `S_OK` the RPC stub duplicates each handle into the client and closes noctty's original. noctty therefore drops those two handles at the point of return and never closes them again. Closing them afterwards is a double close, and `std.os.windows.CloseHandle` asserts that `NtClose` succeeded, so it aborts the process before the adopted session can become a window. ## Taskbar jump list - Recent: up to 10 absolute local working directories reported by shell pwd/OSC integration, newest first. Selecting one launches noctty in that directory. Entries persist in `%LOCALAPPDATA%\noctty\jump-list-recents.json`. To clear them, close noctty, delete that file, and start noctty once so Windows receives the empty list. - Profiles: the detected shell profiles. Selecting one launches noctty with that profile's command. ### Explorer context menu | Explorer target | Classic verb key | | ----------------- | ----------------------------------- | | Selected folder | `Directory\shell\noctty` | | Folder background | `Directory\Background\shell\noctty` | | Drive | `Drive\shell\noctty` | Each verb is named `Open noctty here` and launches `noctty.exe` with `--single-instance=false --working-directory="%V\."`. The explicit new instance keeps the selected path in the launched process, so UNC folders do not cross the single-instance IPC boundary, which rejects UNC working directories. The trailing `\.` is required: Explorer expands `%V` for a drive root as `C:\`, and a backslash right before the closing quote would be read as an escaped quote when Windows splits the command line. The installer writes the verbs under `HKA\Software\Classes`. The current administrative install mode maps `HKA` to `HKLM` and `{autopf}` to the common Program Files directory; in non-administrative mode Inno Setup maps `HKA` to `HKCU` and `{autopf}` to the user Program Files directory. Portable users opt in per-user under `HKCU\Software\Classes` with `noctty +register-shell-menu` and remove the same six owned keys with `noctty +unregister-shell-menu`. The running app never registers these verbs itself and never writes them to `HKLM`. A per-user verb wins over a per-machine one, because `HKCU\Software\Classes` overrides `HKLM` in the merged `HKCR` view. So if you registered the verbs from a portable build and later install noctty per-machine, the per-user copy keeps pointing at the portable executable until you remove it: before deleting a portable copy, run `.\noctty.com +unregister-shell-menu` as the user that registered the verbs. The installer's uninstaller removes only its own registration, and `+unregister-shell-menu` cannot remove the installer's all-users one (it never writes or deletes `HKLM`): on an installed copy it reports the current user's keys as already absent and then says the all-users entries remain, which only uninstalling noctty removes. On Windows 11 these classic verbs appear under **Show more options**; `Shift+F10` opens the classic menu directly. There is no top-level modern command. Shipping one requires a sparse MSIX manifest with package identity, an `IExplorerCommand` COM server, `windows.comServer` plus `Windows.FileExplorerContextMenus` (`windows.fileExplorerContextMenus` in the manifest) extension registration, and a signed package whose Publisher matches the signing certificate. The current self-signed release certificate must be trusted explicitly on each machine before Windows accepts such a package. ## Notifications and progress Desktop notifications use WinRT toasts when available and fall back to in-app banners when Windows notification policy, Focus Assist, app identity, or runtime availability prevents a toast. `desktop-notifications` gates every toast, including command-finish toasts, which additionally need `notify-on-command-finish` and a `notify-on-command-finish-action` that includes `notify`, and [program status](#program-status-osc-7501) toasts. Command-finish and program status toasts focus the originating pane when clicked; OSC 9 and OSC 777 toasts are display-only. Terminal progress reports map to Windows taskbar progress for the active surface in each host window when `progress-style` is enabled; with `progress-style = false` the sequences are silently ignored. Terminal apps can also set in-terminal progress state through Ghostty's shared VT/OSC support. ### Program status (OSC 7501) Programs that speak the [program status protocol](https://www.superlogical.com/rex/docs/build/program-status), such as coding agents, build tools and deploy scripts, tell noctty whether they are idle, working (with optional progress), done, blocked on you (for a permission, an answer, or a sign-in), or failed. noctty answers the protocol's support query (`OSC 7501 ; ?`) and advertises it with the `Pst` terminfo capability, so a program can check before it reports. - **Tab badge.** A tab shows the most urgent status of the programs in its panes: a ring for working (a bright arc for the progress, when reported), a filled amber dot for blocked, a green check for done, and a red cross for failed. Each state has its own shape, so high contrast, which draws every badge in the text color, still tells them apart. A step that finishes or fails inside work that is still going, such as one region of a deploy whose parent record is still working, doesn't show or notify; the work reports when it ends. - **When it clears.** Working and blocked last until the program reports something else, or until it ends: a new shell prompt (OSC 133 A, which noctty's shell integration emits) or the shell exiting. Done and failed stay until you look at the tab, meaning the tab is active and its window is in the foreground. A full reset (`ESC c`) clears everything. - **Taskbar.** The active pane's root status drives the taskbar button like OSC 9;4 does: working is normal or indeterminate progress, blocked is the paused (yellow) bar and failed the error (red) bar. Once a program has reported status, OSC 9;4 from the same terminal no longer changes the taskbar until a full reset, as the protocol asks. `progress-style = false` turns this off too. - **Notifications.** When a tab you are not looking at becomes blocked, done or failed, noctty shows a toast that names the program, the tab and, with more than one window open, the window; clicking it focuses that pane. It follows `desktop-notifications`, and it is rate-limited: at most one per pane every 5 seconds and three across noctty every 10 seconds. One the limit holds back arrives as soon as the limit allows, with what the pane shows then, if it still needs you and you haven't looked. - **Go to the tab that needs you.** The `goto_attention` action, also in the command palette as "Go to Tab Needing Attention", focuses the most urgent other tab: blocked, then failed, then done, and among equals the next one after the current tab, across windows. Done and failed clear once you look, so repeating it works through them. It has no default key binding; bind it with, for example, `keybind = ctrl+shift+g=goto_attention`. `noctty +perform-action goto_attention` runs it from scripts. - **Screen readers.** Each tab's UI Automation `ItemStatus` reads the status, such as "claude needs permission: Allow edit to main.rs?". A new state, request or result raises a property-change event; progress updates don't. Program text shown outside the terminal (in toasts and UI Automation) has bidirectional overrides and invisible formatting characters removed and is cut to 240 bytes. noctty never sends anything a program reported back to a program. With Windows' built-in console host, replies to OSC queries, including this one, don't reach Windows console programs; the ConPTY that noctty ships delivers them. Programs may also report without asking first. ## Power and battery noctty reads AC/battery and saver state from `GetSystemPowerStatus` and subscribes to Windows power-setting notifications for the power source (`GUID_ACDC_POWER_SOURCE`) and Battery Saver (`GUID_POWER_SAVING_STATUS`). It also registers for Windows 11 Energy Saver (`GUID_ENERGY_SAVER_STATUS`), which can engage while plugged in; Microsoft currently documents that GUID as prerelease, so registration is best-effort and has no effect on Windows builds that lack it. Battery Saver and Energy Saver are tracked as two independent flags and combined with OR only when pacing is decided, so one turning off cannot cancel pacing the other still requires. `GetSystemPowerStatus` reports only Battery Saver, so the fallback query carries the last notified Energy Saver value forward instead of clearing it. A fallback query runs at most once every 30 seconds. None of this has been verified on a machine with a battery; see the [status caveats](status.md#known-caveats). - `unfocused-render-fps` caps presentation for visible, unfocused surfaces. Default `30`. Values below `1` are treated as `1`; from `125` up an unfocused surface is capped no more than a focused one (saver pacing still caps both). The cap holds on average. Frames the renderer's own timers bring (a deferred update, a custom-shader animation) wait for the system timer tick, 15.6 ms by default, so one can land up to a tick late and the next comes that much sooner, and such frames never come faster than one a tick (64 fps). - `power-saver-rendering` accepts `auto`, `on`, or `off`. Default `auto`, which follows Windows Battery Saver or Energy Saver; `on` forces saver pacing and `off` disables it. `auto` does not key off battery power alone: running unplugged with saver off keeps the normal cadence, because throttling a focused terminal just because a laptop is unplugged is a far more aggressive default than following the saver signal the user opted into. Focused surfaces keep their normal cadence when saver pacing is inactive. Saver pacing caps presentation at about 30 fps. Minimized and DWM-cloaked host windows do not present until they become visible again. Focus is per surface, not per window. In a split, only the pane with keyboard focus presents at the full rate. The other panes count as unfocused even though you can see them, so a split tailing fast output next to your active pane is capped at `unfocused-render-fps` (30 by default). Raise that value if you watch live output in a background split. Cloak and uncloak have no window message, so noctty hooks the documented `EVENT_OBJECT_CLOAKED` / `EVENT_OBJECT_UNCLOAKED` WinEvents with a single `SetWinEventHook` scoped to this process and the UI thread. Without it, a virtual-desktop switch that uncloaks a background window need not send `WM_ACTIVATE`, `WM_SHOWWINDOW`, or `WM_WINDOWPOSCHANGED`, and since hidden surfaces skip rendering entirely there would be no path back to visible. Those messages remain as a fallback for a missed event or a failed hook registration, which degrades to the previous behaviour rather than failing window creation. The callback forwards which event fired; the refresh it triggers re-queries `DwmGetWindowAttribute(DWMWA_CLOAKED)`, and if that query fails the observed transition is used instead of the last known cloak state, because a stale "cloaked" across an uncloak would leave the window hidden with nothing left to re-query it. This hook path is argued from the Win32 contract and covered by unit tests over the pure event filter and the pure cloak-resolution policy. It has not been observed on hardware; see the [status caveats](status.md#known-caveats). Set `NOCTTY_RENDER_TRACE_FILE` to an absolute output path to write a JSON render trace when the first traced surface is destroyed. Presented fps is `(swap_buffers_count - 1) * 1000 / (last_swap_at_ms - first_swap_at_ms)` when at least two swaps are present and the time difference is positive. ## Keyboard Key events are translated with the active Windows keyboard layout, so the terminal receives the text a key produces on that layout rather than a US-layout approximation. Control chords are encoded from the unmodified layout text: `ctrl+c` sends `0x03`. While no application has enabled the Kitty keyboard protocol, `ctrl+m`, `ctrl+i` and `ctrl+[` send their console C0 bytes (`0x0D`, `0x09`, `0x1B`), so they are the same as Enter, Tab and Escape, and `ctrl+,` and `ctrl+.` have no C0 byte and send their plain text. An application that enables the protocol, as Neovim and Helix can, receives the fixterms `CSI ;5u` form for these chords and can tell them apart. In ConPTY's Win32 input mode the keys travel as console key records instead; see [windows-vt-conformance.md](windows-vt-conformance.md#measured-win32-input-mode-records). AltGr is a layout shift, not a Ctrl+Alt chord. Windows synthesizes left Ctrl plus right Alt whenever AltGr is pressed, and noctty drops that synthetic pair so `AltGr+` produces the layout character (or nothing) instead of a Ctrl+Alt terminal sequence. The collapse applies only on layouts that define AltGr mappings, the only layouts where Windows injects the synthetic Ctrl. On a layout without AltGr, such as plain US, left Ctrl plus right Alt can only be a chord you pressed and passes through as one. On a layout with AltGr, the modifier state noctty reads cannot tell the two apart, so left Ctrl plus right Alt is always read as AltGr there; use left Alt or right Ctrl for a deliberate Ctrl+Alt chord. An Alt chord does not ring the Windows default beep. `TranslateMessage` turns the key message of `Alt+` into a `WM_SYSCHAR`, and a window that lets that message reach `DefWindowProc` gets `SC_KEYMENU`: with no menu bar to match, the shell plays its default sound on every chord, which is what `Alt+/` and `Alt+w` in Neovim did before the fix for #250. The terminal surface now consumes the message, since the chord was already encoded from the key message. That sound is the shell's, not the terminal bell, so `bell-features` never affected it. The same chord typed into the tab strip, the search field or the Settings window is silenced one step later, when the window answers the empty menu loop's `WM_MENUCHAR`. `Alt+Space` still opens the window menu on a decorated window, which Windows reserves for moving and resizing the window from the keyboard; a key that matches nothing inside that open menu leaves it open and, measured on Windows 11, does not beep either. A `keybind` on `alt+space` runs its action and then opens that menu as well, as it did before. While a program has the Kitty keyboard protocol's `report_all` flag enabled, typed text travels on the physical key event rather than a separate character commit, so the press and release a program pairs share one key identity. This includes AltGr characters and dead-key sequences. `key-remap` affects keybinds and terminal encoding but not `global:` hotkeys, which register the literal chord you configured. Console applications reached through ConPTY see these sequences through conhost's own decoder, which does not translate every form back into a console key record. `[Console]::ReadKey()` therefore reports less than the terminal sent; applications that read the byte stream directly see everything. Measured on the in-box conhost of Windows `10.0.26200.0` and on the bundled OpenConsole `1.24.260710001`, an unrecognised CSI-u key encoding is not dropped: ConPTY hands the child the same bytes noctty wrote. Older in-box conhosts, including Windows 10 build 19045, were not measured. See the [key encoding differential](windows-vt-conformance.md#measured-master-to-child-key-encoding-differential). `Ctrl+F6` and `Ctrl+Shift+F6` run `cycle_focus_region` and move keyboard focus between the terminal pane, the tab strip, the docked search query, and the host banner, skipping whatever is not on screen. A bare `F6` is left to the program in the pane (htop, Midnight Commander). `Esc` returns to the terminal. In the tab strip, Left/Right/Home/End move between tabs, Enter or Space activates one, `Delete` closes one, `F2` renames, and Ctrl with an arrow or Home/End moves the tab. These chrome keys are fixed: chrome controls are native windows that never reach the keybind path, so rebinding `cycle_focus_region` changes only the terminal end of the cycle. The command palette and other overlays keep their own focus handling and are not part of the cycle. ## Windows, tabs, and splits The native Win32 host window provides a tab bar with overflow handling, same-window drag reorder, and exact-pane drag-to-split with reversible subtree transfer; horizontal and vertical splits; per-monitor DPI handling; a DWM dark caption; high-contrast palette switching; IME support; drag-and-drop of files, plain text, URLs, and HTML into a pane; and native right-click context menus. The dark caption is chosen at runtime, not by build number: `DWMWA_USE_IMMERSIVE_DARK_MODE` (20) is tried first and the legacy attribute (19) is used when DWM rejects it. Windows 11 additionally gets an integrated title bar (app-owned caption with native caption actions and Snap Layout hover) while the tab bar and window decorations are visible; `window-show-tab-bar = never` falls back to the stock caption, as do older builds. `window-decoration = none` removes the title bar and the window border. On Windows 11 the window keeps its frame otherwise: it resizes from its edges (the top edge is a thin band along the top of the window), keeps its shadow and rounded corners, snaps, maximizes to the work area, and has the Alt+Space window menu. With no title bar to drag, move it with Snap (Win+arrow keys) or Alt+Space, then Move. On Windows 10 the window is frameless: no border, shadow, or resize edges, no Alt+Space menu, and maximizing it covers the taskbar. The quick terminal is frameless on both, so it sits flush with its screen edge. The universal palette puts actions, live tabs, panes, Windows profiles, named layouts, and native settings in one fuzzy-ranked, keyboard-driven list. Type a prefix to filter one category: `>` actions, `@` tabs, `/` panes, `~` profiles, `:` settings, `%` themes, `!` recent commands, `^` layouts, or `?` for help. The native Settings window stages edits until Save (or Discard changes) and patches your config without rewriting unrelated text. Details for both are in the [capability matrix notes](windows-capability-matrix.md#notes). ## Running elevated Use **New Elevated Window** in the command palette, or bind `new_window_elevated:`. The empty payload uses the current/default Windows shell profile; `new_window_elevated:` selects a detected profile by key. Windows shows a UAC prompt before the new process starts. Cancelling the prompt leaves the current window unchanged. The relaunch call is synchronous, so the initiating window is unresponsive while the UAC prompt is open. The roadmap originally asked for a per-profile `run elevated` flag. Windows profiles in noctty are detected from installed shells rather than declared in a user-authored profile schema, so there is no profile property to set. The per-profile equivalent is a `new_window_elevated:` binding. An elevated noctty is a separate process and a separate window. Its title is prefixed with `Administrator: `. Elevated and non-elevated tabs cannot be mixed in one window: Windows isolates the processes with UIPI, and injecting a shell across that boundary is not a supported design. Single-instance IPC is scoped by integrity level. A normal launch and `+new-window` forward only to an existing noctty at the caller's integrity level. From an elevated shell, `+new-window` therefore opens in the elevated group (and starts an elevated noctty when that group is absent). A default-terminal handoff does not use this forwarding at all (see [Default terminal](#default-terminal)), so it never joins an integrity group. It has not been run from an elevated console, and whether COM would serve such a console from a per-user registration is not established. The automation surface is scoped identically: `+list-windows` sees only its integrity group and `+perform-action` can target only windows in that group. The same commands from a non-elevated shell see only the non-elevated group; neither direction silently forwards or automates a window across the integrity boundary. Toast activation, Explorer, and jump-list launches start a medium-integrity process. They cannot activate into an existing elevated window and instead use or start the `.medium` group. A split-token elevated process (`TokenElevationTypeFull`) does not read or write `%LOCALAPPDATA%\noctty\session-state.json`. It never restores an earlier workspace, overwrites the non-elevated saved workspace, or gets resurrected by session restore. With UAC disabled, or under the built-in Administrator account, Windows reports the process as elevated but uses the default token elevation type. In that case the `Administrator: ` title prefix and `.elevated` pipe grouping still apply, while session restore remains enabled because there is no non-elevated split-token counterpart to protect. Configuration and other per-user state still resolve from the same `%LOCALAPPDATA%\noctty\` profile as the non-elevated process. An elevated noctty loads the same `%LOCALAPPDATA%\noctty\config.ghostty` and the same explicit `--config-file` overrides as a non-elevated process. Those files remain writable by the ordinary user account, so anything that can modify the configuration, including `command`, `shell-integration`, and theme paths, runs at high integrity the next time an elevated window opens. At process startup, noctty restricts bare-name DLL resolution to the application directory, System32, and explicitly added DLL directories. The current directory and inherited `PATH` are excluded from that search order. UIPI also prevents files from being dragged from non-elevated Explorer into an elevated noctty window. No message-filter exception is installed; use a shell command or an elevated file manager when the elevated terminal needs a path. Global bindings remain desktop-wide Win32 `RegisterHotKey` registrations. Whichever noctty process registers a chord first owns it, regardless of the process integrity level; a later process logs a registration conflict. The losing process retries on configuration synchronization or restart, not automatically when the winner exits. ## Session restore and recovery Session restore persists host windows, tabs, split layout, selected profiles, working directories, and explicit titles. It does not restore child process state. Terminal contents come back only when `window-save-state-scrollback` is set to a nonzero line count; the default is `0` (off) because terminal output becomes data at rest and can contain secrets. The value is a request clamped to 10,000 lines, not a guarantee: all panes share a 512 KiB encoded budget, each pane is pre-limited by its current width, and any line over 16 KiB is omitted, so wide panes and panes captured later can keep fewer lines. Snapshots are plain text without colors or styles. Soft-wrapped rows become separate hard lines, a row containing invalid UTF-8 or control bytes other than tab is dropped whole rather than sanitized, and a capture taken while the alternate screen is active records the TUI screen instead of the shell history. Each restored pane ends with a `--- RESTORED SNAPSHOT END | ...Z ---` separator carrying the capture time, and the whole snapshot sits above the live prompt in scrollback so the shell's startup repaint cannot destroy it. Engaging `toggle_secure_input` once excludes that pane from snapshots for the rest of the session, even if the indicator is later turned off. One noctty process owns the saved session: the first one started normally, which restores it (or opens its first window when there is nothing to restore). Only that process saves it, on exit, at sign-out, shutdown or reboot, and when the display configuration changes. The save at sign-out, shutdown or reboot is the last one and never deletes the file, even with no windows left: the sign-out also ends the shells, so an empty window list then says nothing about what was closed on purpose. A launch that forwards to a running noctty, a window opened with `--working-directory` (Explorer's "Open noctty here" and the jump list's recent folders) or with `-e` or `initial-command`, the startup profile picker, a `--launch-layout` or `initial-window = false` run, a default terminal handoff and a second instance under `single-instance = false` start without the saved session and leave it untouched when they exit. When one of these is the first noctty running, windows later forwarded into it are not saved either, and the saved session stays as it was until a normally started noctty owns it again. The owner holds `session-state.json.lock` while it runs, so a second instance does not restore the same windows again. If the session-state file is unreadable, noctty moves it aside to a sibling with a `.corrupt` suffix, logs the failure, and starts with a fresh window. When an earlier quarantine file already exists, a numeric suffix is added (`.corrupt.1` up to `.corrupt.4`). Once all five exist, the one written longest ago is replaced, so a file that keeps failing cannot fill the folder. If the move fails, the original file is left untouched. A field this build does not know is skipped, so a file from a build that added one within the same schema version still restores, whether it sits on a window, a tab, a pane or beside a node's `pane` or `split`; a window `state` name it does not know restores as a normal window. A new kind of node or a new `axis` name is not additive and needs a new `schema_version`. A file with a newer `schema_version` is not unreadable, only not understood: noctty leaves it exactly as it is, starts with a fresh window, and does not save a session over it for the rest of that run. The startup history (`startup-attempts.json`) is read the same way, and `integration.ps1` is not replaced by a build whose copy declares an older `noctty-integration-revision`. Builds that predate this still parse strictly, so downgrading to one while a saved state contains `scrollback` can quarantine that file as `.corrupt`. A restored window comes back at the rect it was saved with, in screen coordinates, and as maximized if it was or was minimized from maximized. If the title bar would no longer be reachable on any monitor (a monitor was unplugged or the layout changed, or Windows capped an extreme size or position), the window moves to the nearest monitor's work area, its size capped to it. A window with a caption's worth of title bar (48 px wide) on some monitor stays where it was, even if the rest of it hangs off the screen. A snapshot that merely exceeds the limits above (too many lines, a line over 16 KiB, or the shared budget) is dropped on load and the window's layout still comes back. A snapshot of the wrong shape, such as a hand-edited `"lines": "not-an-array"`, makes the whole document malformed and takes the quarantine path instead, so that window's layout is not restored. Nothing is deleted either way. Three consecutive pre-ready startup failures select an ephemeral safe mode: built-in config, no session restore. `noctty --safe-mode` picks it for one launch. Safe mode never overwrites your config or quarantined state. Each launch records in `startup-attempts.json` the noctty version that started it, when it began restoring the saved session and whether that restore finished. When all three failures ended during a restore, safe mode alone would only postpone the next three, so the safe-mode launch also renames `session-state.json` to `session-state.json.restore-failed` (or `.1` up to `.4` if earlier ones exist) and its notice says so; the next normal launch starts with a fresh window. Nothing is deleted or replaced: rename the file back to try that session again. A session another running noctty owns, or one with all five `.restore-failed` names already taken, is left where it is, and that safe-mode launch says only that the session was not loaded. ## Named layouts A named layout captures the focused window's tabs, split trees, selected profiles, working directories, and explicit pane and tab titles. It omits window position, size, and state. Each layout is the session-state JSON schema with exactly one window, stored as `%LOCALAPPDATA%\noctty\layouts\.json`. Per-pane commands come from the saved profiles; layout files have no separate command field. - Names accept ASCII letters, digits, spaces, dots, underscores, and hyphens, up to 64 bytes. Leading or trailing spaces and dots are rejected, as are Windows reserved device names such as `CON` or `COM1`. - A corrupt or structurally invalid layout file is quarantined with the same `.corrupt` rules as session state. A transient read failure is reported without quarantining, so a locked or briefly unavailable layout is not moved aside. - A layout that would open more than 16 tabs or 64 panes is refused without quarantine. - Window position, size, and maximized state are stripped before validation and ignored on launch; a layout describes a shape, not a placement. Partial or stale geometry such as `x` without `y` or a zero width does not quarantine an otherwise valid shape. Treat the `layouts\` directory as config-level trust. Launching a layout starts one shell per pane, using the working directories and profiles the file names. Anyone who can write to that directory can influence what your next layout launch runs, exactly as they could by editing your config. Bind `save_layout:` to save or atomically replace a layout, and `launch_layout:` to open it in a new window. Saved layouts also appear in the command palette as `Launch layout: `. From the command line, `noctty +new-window --launch-layout=` forwards the request to a running instance or launches cold when none is running. The warm (running-instance) half of that command is not a forwarded configuration argument. It is a dedicated IPC request whose whole payload is the layout name, revalidated on the receiving side against the same character, length, traversal, and reserved-device rules the CLI applies, and resolved server-side against `%LOCALAPPDATA%\noctty\layouts\`. So `--launch-layout` cannot be combined with other `+new-window` arguments; mixing them is refused. `launch-layout` is not on the forwarded-argv allowlist and will not be added: a layout names a file that selects profiles, profiles carry commands, and code selection is what that allowlist exists to refuse. The keybind and the command palette do not go over IPC at all. `--launch-layout` is a command-line-only, one-shot option. Setting `launch-layout` in `config.ghostty` is ignored with a warning, because a configuration file is read on every start and the layout would replay on each launch. The layout module also exposes name enumeration and the `+new-window --launch-layout=` argv builder for future jump-list integration; noctty does not currently add jump-list layout entries. ## Updates ```ini auto-update = check ``` The updater checks the configured release feed when the app starts, unless a check succeeded in the last 24 hours, and never replaces binaries silently. A copy that stays open re-runs the same check every six hours while a window is open and no staged update is waiting; the 24 hour limit and a dismissed version still apply, so most of those ticks make no request. The feed defaults to noctty's GitHub Releases API and can be changed with `auto-update-feed-url`; checksum, Authenticode, and pinned-publisher-key verification stay mandatory whatever the feed host. For tests and diagnostics, `NOCTTY_UPDATE_FEED_URL` overrides the compiled-in default when no explicit `auto-update-feed-url` is set. The precedence is explicit config, then the environment variable, then the compiled-in default; a blank or whitespace-only value at either level falls through to the next, and a value that is not a valid HTTPS URL is ignored with a warning. Changing the effective feed discards the previous feed's cached release, dismissal, and staged installer. When a newer stable version exists, `check` mode shows a notice that opens the release page when clicked. The updater is the only part of the app that goes online on its own; there is no telemetry and no analytics. [PRIVACY.md](../PRIVACY.md) covers what its requests send and the connections you start yourself, such as links and SSH entries. Update requests allow 30 seconds for connection setup (including DNS and TLS), the response headers, and each body read. A stalled request is canceled before the worker finishes, so a later check can retry. A download that keeps making progress may take longer than 30 seconds. Requests use Windows' certificate trust with TLS 1.2 or later; checksum and publisher verification still apply. Upstream Ghostty is leaving GitHub, so the maintainer periodically checks that the `upstream` remote and the release-feed host are still live and re-points them if either moves. `auto-update = download` downloads only stable Windows releases that ship architecture-specific SHA256 metadata, verifies the selected installer or portable ZIP against that manifest, and requires a valid Windows Authenticode signature before staging it under the local noctty state directory. Portable updates additionally require a signed payload-manifest asset whose SHA-256 entries cover every extracted file, and every extracted `.exe`, `.com`, and `.dll` must pass its own Authenticode check. The signature check is not generic: the signer's public key must match a SHA-256 SPKI pin compiled into the app, so a binary signed by any other key is rejected even when its signature is otherwise valid. Unsigned binaries fail that verification too. The bundled Microsoft `conpty.dll` and `OpenConsole.exe` are the two exceptions, because noctty never re-signs them: each must match the SHA-256 pinned in `dist/windows/conpty-redist.json` and carry a signature that chains to a trusted root, which is stricter about the chain than the pinned-publisher path. Every other PE keeps the publisher pin. See [ADR 0005](adr/0005-pin-updater-publisher-public-keys.md) for the pinning and key-rotation rules. Only a setup-managed copy or one with the `noctty.portable` marker stages anything. A copy installed another way (Scoop, a ZIP extracted without the marker) cannot apply a staged update, so `download` mode downloads nothing for it and its notice opens the release page. A version that is already staged and still verifies is reused instead of downloaded again, whether the check was forced or answered from the 24 hour cache, and dismissing the notice for a staged update keeps the staged files and records the dismissal. For installer-managed installs, the update notice can launch the verified staged installer. Applying an update is always user-initiated. The notice says that installing closes all windows and running sessions, and **Install** asks first, like quitting, when a terminal still has a running process. It then re-verifies the staged installer, records apply intent, launches the installer elevated (UAC may prompt), and exits the app. The silent setup starts noctty again when it finishes, through Explorer so that it is meant to run as the logged-on user and not elevated, so an update that works ends with a new window. (The wizard's "Launch noctty" box starts it the same way.) That de-elevation is the intended result and has not been measured on a real install. If a later start finds that the build is still the old one after an Install, and that Setup is no longer running, a banner says the update did not finish and names the setup log (`updates\\logs\apply.log` under the noctty state directory); the staged installer is kept, so **Install** is offered again. A banner that says Setup left no log means it started but could not write one, or was stopped first. Only a setup-managed copy reports this, because the update state is shared by every copy of the user's. Declining the elevation prompt does not quit noctty or record anything: the banner just says the installer could not be launched. For portable installs, **Apply** re-verifies the staged ZIP payload, records the pending update, and exits noctty. On the next launch, noctty backs up the files being replaced, swaps in the staged payload, and starts the new build. The new build confirms the update only when its version matches the staged target; otherwise noctty restores the backup. A new build that exits with an error during the bounded startup check, or a swap that remains unconfirmed on a later launch, also rolls back and relaunches the previous build. If recovery files are lost after a swap, noctty abandons the unrecoverable transaction, launches the installed build, and shows a warning with manual recovery guidance. `NOCTTY_PORTABLE_UPDATE_BYPASS=1` is the manual escape hatch for portable-update startup recovery: set it only for the process being launched to skip that recovery attempt. This bypass does not verify, complete, or roll back the update, so reinstall or update manually afterward if the installed build is inconsistent. Portable apply fails closed until a release publishes that signed complete-file manifest. Releases without it are not downloaded or staged for portable apply; the update notice opens the normal GitHub release page for manual updating. ## Quick select & copy mode `Ctrl+Shift+Space` runs `toggle_quick_select` for the focused terminal surface. Quick select scans the visible viewport once when it opens and labels matching URLs, paths, git SHAs, IP addresses, and UUIDs. Type a label to select its target. Backspace removes one typed label character; Esc cancels and returns keyboard focus to the terminal. Quick select also closes on resize, on a click, and when the surface loses focus. Completing a label copies the matched text to the clipboard. Hold Ctrl while completing to open a target that begins with an allowed URL scheme or names a file or folder on this machine (`C:\dir\file.txt`, `.\src\main.zig:12:5`, `src\a.cs(3,4)`); other targets, network shares included, are copied. A path opens through the same checks as a Ctrl+click on it, with a trailing `:line:col` or `(line,col)` left off: it must exist and must not be a program or script. When it is refused, a banner says it was not opened. Hold Alt to paste through the normal protected-paste path. Opening is narrower than matching: `file:` URLs are labeled and can be copied, but Ctrl will not open them, because the system handler would run `file:///C:/...exe` straight from terminal output. Alt-paste also runs the same classifier as the drag-and-drop path, so a match carrying shell metacharacters, environment-variable expansion, or control bytes raises the paste confirmation. The same matcher finds paths for Ctrl+click links and double-click selection: `C:\dir\file`, `C:/dir/file`, `\\server\share\file`, `.\file` and `dir\file.ext`, with an optional `:line:col` or `(line,col)`. A path ends at `;`, `,`, `"`, an unbalanced bracket, and a trailing `.`, `!`, `'` or `}`; a pair of brackets inside it, as in `Program Files (x86)`, stays. Spaces inside a path stay part of it: a space continues the path after a word whose last segment has no dot (`Program`), and after one that has a dot (`git.exe`, `Node.js`) only when the next word starts like a folder name and has a backslash in it (`Tools\x.exe`), so a URL or another path after a file name is its own link. So `C:\a.txt copied` ends at `.txt` and `C:\Program Files\Git\bin\git.exe failed` at `git.exe`, while `C:\Program Files\Node.js Tools\x.exe` stays whole. A path whose last segment has no dot still takes the words after it (`C:\Users\amant and then` is one match), because that cannot be told from a folder with a space in its name. A path inside single or double quotes ends at its closing quote. A quick select with nothing to label, or with too many targets to label without overlap, says so in the banner. Targets are re-checked against the live screen when you act. If the running program rewrote a labeled region while the overlay was up, that target is ignored, so you only act on what was on screen. `quick-select-patterns` is repeatable and accepts bare regex values or quoted Zig string literals. An empty value clears configured patterns; when the configured list is empty, the built-in patterns apply. `quick-select-alphabet` sets the unique printable ASCII characters used for labels. It requires at least two characters and treats ASCII letters that differ only by case as duplicates. Use Zig string literal syntax to preserve leading or trailing spaces. `Ctrl+Shift+X` runs `toggle_copy_mode`. Copy mode starts a selection at the terminal cursor when it is visible, or at the bottom edge of a scrolled viewport. | Keys | Action | | -------------------------------------- | ----------------------- | | `h`/`j`/`k`/`l` or arrow keys | Move | | `Ctrl+U`/`Ctrl+D` or Page Up/Page Down | Move by a viewport | | `g`/`G` or Home/End | Jump through scrollback | | `0`/`$` | Jump within a line | | `y` or Enter | Copy and exit | | `q` or Esc | Cancel | Other keys are consumed instead of reaching the PTY. Mouse selection and reporting keep working: a left click replaces the copy-mode selection, and a program with mouse reporting enabled still receives mouse escape sequences. Legacy non-OLE file and text drop paths are ignored while copy mode owns input; OLE drops remain mouse gestures and continue through paste inspection and confirmation. These bindings live in the `copy_mode` key table and can be replaced with normal `keybind = copy_mode/...` entries. `copy_mode` is one mode, not a re-enterable table: activating it again from a nested table does not stack a second copy mode, and leaving copy mode unwinds every table pushed on top of it. ## Mouse input Right-click follows `right-click-action`. With its default `context-menu`, mouse-reporting applications receive the click; hold Shift to use the terminal menu unless the application is configured to capture Shift. `ignore`, `copy`, `paste`, and `copy-or-paste` perform their configured action without opening a menu. Wheel reports use the position carried by the wheel event. The number of reports per notch retains the Windows scroll-lines setting and the configured multiplier. If a mouse-reporting TUI scrolls too quickly, try `mouse-scroll-multiplier = discrete:1`; this also slows normal viewport scrolling. ## Quick terminal and global hotkeys The quick terminal uses the same `toggle_quick_terminal` action and `quick-terminal-*` configuration family as Ghostty where the settings map to Windows. Global keybinds use Win32 `RegisterHotKey` while noctty is running. Windows or other applications may reserve hotkeys first; check logs when a global binding does not register. `quick-terminal-keyboard-interactivity = exclusive` maps to focused input on Windows. Global keyboard capture is intentionally not implemented. ## Automation noctty exposes a local automation surface over the same single-instance IPC pipe that `+new-window` uses. The verbs, JSON schema, exit codes, and policy are in [automation.md](automation.md). ```powershell noctty +list-windows noctty +perform-action new_tab noctty +perform-action --surface-id= toggle_fullscreen noctty +new-tab --window-id= noctty +new-split --surface-id= --direction=right noctty +focus --surface-id= noctty +send-text --surface-id= -- "hello" ``` ### Pipe security The pipe accepts only processes running as the same user: it is created with `PIPE_REJECT_REMOTE_CLIENTS`, a protected DACL that grants access to the owning token's SID alone, and a `NO_WRITE_UP` mandatory-integrity label at the process's exact integrity level. An elevated instance therefore refuses a non-elevated client of the same account at `CreateFileW` (`win32=5`); the client reports "No matching noctty instance is listening" and `+new-window` starts its own instance instead. An elevated client still reaches the elevated instance. Clients authenticate the server too, because the pipe name derives from `--class` and any local account could create it first. Before writing, a client reads the owner SID and integrity label from the connected pipe object; the owner must match the client's SID and the integrity must be at least the client's, or the client treats it as no reachable instance. Clients also connect with `SECURITY_SQOS_PRESENT | SECURITY_IDENTIFICATION`, so a squatting server could learn who connected but could not use the token. The elevated endpoint is labelled `NRNW`, so a medium-integrity process cannot open it even for `GENERIC_READ` and cannot occupy its listening instance. Non-elevated endpoints keep the write-only-up label, where the same occupancy behaviour still applies within one integrity level. An elevated client additionally requires the server to be elevated, owned by the same token user, and running the same executable image; the image check is defence in depth, not a boundary, and refuses a forward between two different noctty installations. None of this is a privilege boundary. Code already running as your user can list your windows, perform allowlisted actions, and open new windows. Treat the channel as you would your own shell. ### Commands `+list-windows` returns the `noctty.windows.v3` schema: instance, window, tab, and pane metadata, including nullable pane titles and working directories. It never includes terminal text, scrollback, selection, clipboard contents, pending shell input, or pane process IDs. `+perform-action` takes the same names as `keybind` values. `--surface-id` is valid only for surface-scoped actions. The running instance refuses actions that inject terminal input or touch arbitrary files (`text`, `csi`, `esc`, `paste_from_clipboard`, `write_screen_file`, `end_key_sequence`, `clear_screen`, `crash`), refuses `undo` and `redo` because they replay a captured action, and leaves new action variants disabled until reviewed. `launch_layout:` and `save_layout:` are allowed; saving is the one action that writes a file, limited to a validated name under `%LOCALAPPDATA%\noctty\layouts\`, written atomically, replacing an existing layout without prompting, and only for the focused target. ### Forwarded arguments `+new-window` forwards command-line arguments to the running instance, which honors only allowlisted window-scoped presentation settings: window geometry and decoration (`--window-width`, `--window-height`, `--window-position-*`, `--window-padding-*`, `--window-decoration`, `--maximize`, `--fullscreen`), `--title`, non-name font settings (`--font-size`, `--font-thicken`), and colors (`--background`, `--foreground`, `--palette`, `--cursor-color`, `--background-opacity`). Anything that runs code (`--command`, `--initial-command`, `-e`, `--input`, `--env`), loads a file (`--background-image`, `--custom-shader`, `--bell-audio-path`), changes the configuration source (`--config-file`, `--config-default-files`, `--theme`), or writes to the terminal (`--keybind`, `--command-palette-entry`, `--enquiry-response`) is refused, and the whole request with it. Keys not on the allowlist, including ones added later, are refused by default. `--working-directory` accepts `home`, `inherit`, `~/...`, or a drive-letter absolute path. UNC paths are refused so the running instance never authenticates to a remote SMB host; this is a syntax check, so a mapped drive or junction can still resolve off-box. A normal launch while an instance is running drops any argument the instance would refuse, logs a warning, and forwards the rest so you still get a window. The running instance re-checks every argument regardless. ## Crash reports and diagnostics Crash dumps stay under `%LOCALAPPDATA%\noctty\crash`, and noctty never uploads anything from it. On Windows the Sentry initialization path is a no-op. Instead, noctty installs a local unhandled-exception filter that writes `.dmp` minidumps for process-level crash exceptions. Some hard-abort paths may still terminate before Windows can produce a dump. ```powershell noctty +crash-report noctty +diagnostic-bundle --output=noctty-diagnostics ``` `+crash-report` reads whatever is there. Dumps can contain sensitive memory from the crashed process; review them before sharing. `+diagnostic-bundle` writes a local-only, inspectable support bundle. Terminal content, commands, environment, working directories, and config values are excluded by default. Crash dumps are excluded too unless you pass the explicit `--include-crash-dumps` flag. ## Troubleshooting SmartScreen warns on release downloads because the current signing certificate is self-signed and carries no publisher reputation. [getting-started.md](getting-started.md#about-the-smartscreen-warning) explains the warning and how to verify a download against its checksum file first. Desktop notifications missing: check Windows notification settings, Focus Assist, and whether you are running an installed build with Start menu shortcuts. In-app banners should still appear for important app notices. Global hotkey fails: another application or Windows itself owns the same hotkey. Pick a different trigger or close the conflicting app, then reload config. WSL fails to launch: set `command = wsl.exe` explicitly. If it still fails, verify the distribution starts in a normal PowerShell session first. ### Neovim types base64 after an OSC 52 paste Neovim's OSC 52 clipboard pastes by asking the terminal for the clipboard. Neovim uses it when `vim.g.clipboard = 'osc52'` is set, and falls back to it when it finds no other clipboard tool and `'clipboard'` is empty, which is common over SSH and in WSL. noctty answers with one escape sequence that carries the clipboard as base64 and ends with `ESC \`. Neovim waits at most `'ttimeoutlen'` milliseconds for the next piece of an unfinished escape sequence. If a longer pause falls inside the answer, Neovim gives up and types what it has so far as keys. The paste fails, the keys run as Normal-mode commands, and the first one that starts Insert mode types the rest of the base64 into the buffer. With `'nottimeout'` or a negative `'ttimeoutlen'`, every OSC 52 paste fails this way, whatever the connection: Neovim splits the answer at the `ESC` of its terminator and gives up on the first part at once. Unlike Vim, Neovim's terminal input does not fall back to `'timeoutlen'` for a negative value; it waits for the rest of a sequence only while `'ttimeout'` is on and `'ttimeoutlen'` is 0 or more, and otherwise processes it immediately (`tk_getkeys` in `src/nvim/tui/input.c`, the same in 0.10 through 0.12). Otherwise, what runs between noctty and Neovim decides how long the pauses are. Measured on Windows 11 with Neovim 0.10.4, 0.11.7 and 0.12.5: - Native Windows Neovim gets the answer from the bundled pseudo console without pauses; a console test program received a 266 KB answer in 16 ms. A `'ttimeoutlen'` of 0 did not leak. - Neovim in WSL gets it from `wsl.exe` in pieces of 16 to 32 bytes, mostly less than a millisecond apart. A `'ttimeoutlen'` of 1 leaked every time, and 2 in one of two runs. - Neovim over `ssh.exe` (OpenSSH for Windows 9.5) to a host on the same machine got the last few bytes 25 to 43 ms after the rest. OpenSSH 9.5 and later hold back small writes to hide keystroke timing (`ObscureKeystrokeTiming`), and the end of the answer is a small write. With nothing typed after the paste, a `'ttimeoutlen'` of 25 or less leaked every time, and 30 to 40 in 4 of 7 runs. - Over SSH to a distant host, the answer can also arrive in bursts about one round trip apart. `ssh.exe` sends it as many small packets, and after a quiet spell TCP sends only a few packets per round trip until its window grows. noctty's clipboard prompt (`clipboard-read = ask`, the default) makes that quiet spell: the SSH connection sits idle while the prompt waits for you. With the paste typed through `ssh.exe` and Allow clicked 3 seconds later over an emulated 60 or 100 ms round trip, the default `'ttimeoutlen'` of 50 leaked every time (13 of 13, from 1 KB to 200 KB). With an emulated round trip of 40 ms or less, none of 6 leaked, and with `clipboard-read = allow` or Allow clicked within 300 ms, none of 17 leaked at a 60 to 100 ms round trip. (That was measured before the prompt began ignoring Allow for its first half second; an answer that quick is no longer possible.) With the default `'ttimeoutlen'` of 50, nothing leaked natively, in WSL, or over SSH to a host on the same machine, for clipboards from 64 KB to 700 KB. For an SSH host more than about 50 ms away, set `'ttimeoutlen'` above the round-trip time; 150 did not leak at 100 ms (0 of 6). `clipboard-read = allow` also avoided it, but lets any program in any tab read your clipboard without asking. If you lower `'ttimeoutlen'` below about 45 for a nearby SSH host, `ObscureKeystrokeTiming no` in your SSH configuration removes the SSH client's pause, at the cost of that protection. Short timeouts also break Neovim's startup. With a `'ttimeoutlen'` of 20 or less over SSH, or 0 in WSL, the replies to Neovim's own startup queries arrive in pieces too and run as Normal-mode keys. One of them contains `r0`, which replaces the character under the cursor. noctty 1.3.130 and earlier could also drop part of a large answer when input arrived while it was being sent, which caused the same symptom at any `'ttimeoutlen'` (#261). Very large pastes into native Windows Neovim fail differently. Neovim reads console input one key event at a time, and a 700 KB paste was still not through after the 10 seconds Neovim waits; keys pressed after it reached Neovim 13 to 14 seconds later. The paste stops with `Timed out waiting for a clipboard response from the terminal`, and nothing is typed. ### ConPTY source Releases since 1.3.125 ship Microsoft's ConPTY pair, `conpty.dll` and `OpenConsole.exe`, beside `noctty.exe`, and `zig build` stages the same pinned pair in `zig-out\bin`. `noctty +version` reports `ConPTY : bundled ()`, or `ConPTY : inbox` with the reason, for example `inbox (conpty.dll is not next to noctty.exe)`. `+diagnostic-bundle` records the same selection and reason (`"fallback": "NotFound"`). Set `NOCTTY_CONPTY=inbox` before launch to force the in-box conhost for diagnosis. When noctty falls back on its own it shows a banner naming the reason, in every build. One fallback happens only inside a running noctty: if the bundled pair loads but fails to create a pseudo console, that process moves to the in-box conhost for every terminal it opens from then on, and its banner says so, while `+version` and the bundle, which run as separate processes, still report `bundled`. The shell still works, but the in-box conhost is a different terminal path. On the Windows builds measured it re-renders output instead of forwarding it, answers DA, DSR and DECRQM itself (reporting synchronized output, mode 2026, as unsupported), never returns the OSC 4/10/11/12 colour or XTGETTCAP replies, and strips Kitty-graphics APC and Sixel DCS payloads. A program that asks for the terminal's colours waits for replies that never come. See the [query differential](windows-vt-conformance.md#measured-query-ownership-differential) and the [transport catalog](windows-vt-conformance.md#conpty-transport-generations-and-mangling-catalog). ### Inline graphics: replies, Sixel and memory - **Kitty graphics replies reach the child as input.** A command that carries an image id or number (`i=` or `I=`) is answered on the child's input with `ESC _ G ... ESC \`, an `OK` or an error, unless `q=1` (suppress success replies) or `q=2` (suppress all replies) is set. A successful delete never replies (`graphics_exec.zig`, `StreamHandler.apcEnd`). noctty has no ConPTY-specific suppression. The 2026-10-10 audit measured the bundled ConPTY handing a native console program (`[Console]::ReadKey` in pwsh, and `cmd`) a lone `\` for such a reply, while the same reply arrived intact through WSL's raw pty; this was not re-measured for this page. A tool run directly under ConPTY that does not parse APC replies should send `q=2`. - **Sixel is passed through but not rendered.** The bundled ConPTY delivers a Sixel DCS payload byte-for-byte (see the [transport catalog](windows-vt-conformance.md#conpty-transport-generations-and-mangling-catalog)), but the terminal has no Sixel decoder: `src/terminal/dcs.zig` handles only tmux, XTGETTCAP and DECRQSS, and the primary device-attributes reply does not advertise Sixel (feature 4). Kitty graphics is the one inline image protocol that draws. - **`image-storage-limit` bounds the terminal's decoded copy only.** The limit (320 MB per screen by default; the primary and alternate screens each have one) is checked against the image bytes the terminal stores (`kitty/graphics_storage.zig`). The renderer's own copies, pending upload data and GPU textures, are not counted against it (`src/renderer` never reads it), so the process can hold a multiple of the limit while images are on screen. The audit measured about 750 MB of growth from 12 zlib-compressed 3000x3000 images sent in a 0.56 MB file. Memory fell after `cls`, and `image-storage-limit = 0` disables the image protocols. ### GPU floor and OpenGL driver issues The default OpenGL backend needs OpenGL 4.3 or newer through WGL and does not automatically switch to another renderer when the active implementation is below that floor. The opt-in D3D11 beta can draw supported text with feature level 11.0 hardware or WARP, without OpenGL. Full rendering of unsupported images and effects requires a working OpenGL fallback; otherwise healthy D3D11 continues drawing text with those features degraded. The OpenGL floor is a common issue in RDP sessions that fall back to software GL (often `GDI Generic` at OpenGL 1.1), VMs without 3D acceleration or a guest GPU driver, and older integrated GPUs whose driver stops before 4.3. With the OpenGL backend below the floor, noctty stops before showing its window and the startup dialog lists the required and detected OpenGL versions plus the renderer and vendor strings the driver reports. The dialog also prints an `Architecture:` line naming the process and native machine, so a bug report separates a native ARM64 build from an x64 build running under emulation. Failures later in OpenGL initialization use the same dialog to name the failed step and any Win32 or Zig error. On x64, try in order: 1. End the Remote Desktop session and launch noctty locally. 2. In a VM, enable 3D acceleration and install the guest graphics driver. 3. Update or reinstall the OEM graphics driver for the active GPU; on an AMD+NVIDIA hybrid system, the AMD driver first, then NVIDIA. 4. On a hybrid-GPU system, force `noctty.exe` to the discrete or integrated GPU in Windows Graphics settings. Steps 3 and 4 do not apply on an ARM64 PC whose GPU vendor ships no desktop OpenGL driver (any Snapdragon PC), where there is no vendor driver to reinstall. Use [Windows on ARM without a vendor OpenGL driver needs the Compatibility Pack](#windows-on-arm-without-a-vendor-opengl-driver-needs-the-compatibility-pack) instead. If startup fails with `LoadLibrary failed with error 126` or the startup dialog reports `Win32 error: 126 (ERROR_MOD_NOT_FOUND)` during OpenGL/WGL initialization, Windows could not load a graphics-driver DLL or one of its dependencies. This shows up most often on AMD+NVIDIA hybrid-GPU laptops while WGL loads the AMD OpenGL ICD from DriverStore; use the driver order in step 3 before retrying. #### Windows on ARM without a vendor OpenGL driver needs the Compatibility Pack Qualcomm Snapdragon PCs ship no desktop OpenGL driver. No Qualcomm implementation has ever appeared in the Khronos OpenGL conformant-products register, so a fresh Snapdragon machine has no OpenGL ICD for WGL to load at all, whatever else its Adreno driver supports. noctty's startup dialog reports that as `Detected OpenGL version: 1.1`, `Detected renderer: GDI Generic`, and `Detected vendor: Microsoft Corporation`, alongside `Architecture: ARM64 process on ARM64 Windows`. Those GL strings are the GDI software fallback Windows uses when no ICD is present, not a driver that stopped short of 4.3. Install the free **OpenCL, OpenGL, and Vulkan Compatibility Pack** from the Microsoft Store, then restart noctty. The pack installs Microsoft's GLon12 mapping layer, which implements desktop OpenGL on top of Direct3D 12 and supplies the ICD that is otherwise missing. Microsoft's own support statement for the pack promises OpenGL 3.3, below noctty's floor, but a measured report on an Adreno X1-85 shows it exposing a 4.6 core profile through the D3D12 path, with the capabilities noctty's renderer needs (vertex-stage SSBOs, `GL_ARB_texture_rectangle`, and `GL_ARB_framebuffer_sRGB`): ```text GL_VERSION : 4.6 (Core Profile) Mesa 26.1.3 GL_RENDERER : D3D12 (Qualcomm(R) Adreno(TM) X1-85 GPU) ``` The Mesa version is whatever the installed pack ships; only the `D3D12 (...)` renderer prefix and a version at or above 4.3 matter. On a Snapdragon PC, or any ARM64 PC whose GPU vendor ships no desktop OpenGL driver, the Compatibility Pack is required for noctty's default OpenGL 4.3 backend; an ARM64 PC with its vendor's own OpenGL driver does not need it. The opt-in D3D11 beta provides a separate hardware/WARP text-rendering path. With the pack, OpenGL comes from a Microsoft mapping layer rather than a GPU vendor's driver, but noctty neither knows nor cares which supplied it. There is no AMD or NVIDIA driver to reinstall on ARM64 and no second GPU to select in Windows Graphics settings, so noctty's startup dialog leads with the Compatibility Pack when it detects an ARM64 native machine, and keeps the Remote Desktop and VM guidance, which applies on any architecture. The dialog and `noctty +version` both report the architecture: `ARM64` for a native build, or `x64 (running on ARM64)` if you installed the x64 build by mistake. Prefer the ARM64 artifacts on a Snapdragon PC. ### Stale installed build When testing a local build, make sure the `noctty` you invoke is the current `zig-out\bin` binary rather than an older Scoop shim or a manually added install directory earlier on `PATH`. A `noctty.com` executable may be selected before `noctty.exe` for CLI invocations, because `.com` ranks before `.exe` in `PATHEXT`. `noctty.com` is the console launcher and the recommended way to run CLI actions. `noctty.exe +` also works: the Windows-subsystem binary attaches to the console it was launched from before running the action, and leaves redirected output alone.