posh makes a shell session portable: it roams across networks and persists across disconnects, so the session you start follows you — survive a sleep, change networks, detach here and reattach from another machine, and the shell and its running processes are exactly where you left them.
It delivers this as a single Rust tool that combines two lineages:
- zmx (terminal session persistence — attach/detach from sessions without killing the underlying processes, window management delegated to the OS window manager), and
- mosh (roaming remote terminal over encrypted UDP that survives sleep, network changes, and intermittent connectivity).
Portability is the two together: mosh-style roaming carries a session across
the network, zmx-style persistence carries it across time and machines,
and the unified host:session namespace addresses both with one grammar.
This repository hosts the rewrite as a Cargo workspace. The original C++
mosh tree is kept under zz-mosh/ as the porting reference (with its own
justfile for the host-lane build: just zz-mosh/<recipe>); the original
zmx (Zig) lives in its own repository.
crates/
posh-term/ standalone terminal emulation library (no dependencies)
posh/ the posh binary
posh-proto/ shared frame/display protocol: Snapshot + new_frame renderer, frame codecs (DumpDiff/MorphDelta), ServerFrame/FrameBody wire types, RFC 0001 caps (github #75)
poshterity/ deterministic terminal recorder/replayer (lib + poshterity bin; posh rec)
mosh-ffi/ C++ FFI oracle: drives mosh's terminal + predictor for differential tests (dev/test only; ADR 0004)
doc/ scdoc man-page sources (man posh, posh-server, posh-client, poshterity, posh(7))
docs/ ADRs, RFCs, feature records (FDRs), plans, and the manual test plan
posht/ interactive terminal-capability test (Go; nix build .#posht)
zz-mosh/ the C++ mosh reference tree (buildable: nix build .#mosh)
A from-scratch Rust rewrite of the ghostty-vt terminal core as a completely
independent, dependency-free library crate. It is pure state: feed it PTY
bytes with Terminal::process, query the resulting screen, and drain query
replies (DA/DSR/OSC/kitty) with take_responses.
Feature set targets kitty parity, with the sequences fish shell integration relies on fully supported:
-
Williams DEC parser state machine; incremental UTF-8 with malformed-input replacement; C1 controls; colon SGR subparams.
-
Primary screen with scrollback ring + alternate screen; wide chars, combining marks, pending-wrap semantics, BCE, margin-aware scrolling, origin mode, tab stops, DECALN, REP.
-
Full SGR including styled underlines (single/double/curly/dotted/dashed), underline color (58/59), 256-color and truecolor in both
;and:forms. -
Modes: DECCKM, DECOM, DECAWM, DECTCEM, alt screen 47/1047/1049, bracketed paste 2004, mouse 9/1000/1002/1003 with SGR/SGR-pixel protocols, focus reporting 1004, synchronized output 2026, IRM, LNM.
-
OSC: 0/1/2 title, 4/10/11/12 palette and dynamic colors (set + query), 7 (cwd), 8 (hyperlinks), 52 (clipboard), 133 (shell-integration prompt marks, as emitted by fish), 9/99 (notifications), 22 (pointer shape).
-
Kitty keyboard protocol: full flag stack (push/pop/set/query) plus a client-side
encode_keycovering legacy and CSI u encodings. -
Kitty graphics protocol: APC G parsing, transmit/place/delete/query, RGB/RGBA/PNG formats, chunked transmission, 320 MB quota, spec ACKs.
-
DCS: DECRQSS, XTGETTCAP; queries: DA1/DA2, DSR, DECRQM, XTVERSION, XTWINOPS 14/16/18.
-
Serialization:
dump_text()(plain text including scrollback) and two escape-stream forms that reconstruct contents, attributes, cursor, modes, title, scroll region, and kitty graphics — images, placements, animation frames — verified by roundtrip tests. They have different contracts and are not interchangeable:dump_vt()targets a freshly constructedposh_term::Terminal— the frame codecs build one per apply. That target may be larger than the source (see the sizing note under Sessions), so the replay must not depend on the target's height.dump_vt_flat()targets a real tty carrying whatever mode state the previous application left behind, so it opens by resetting that state (DRAWABLE_STATE_RESET). It draws only the active grid and never switches the target's screen buffers.
Together these power session replay on attach and remote state sync.
Also implemented: reflow on resize (logical lines rewrap via wrap flags,
wide-char aware, scrollback included; alt screen truncates/pads like kitty),
DECCOLM/DECNCSM column switching, kitty graphics relative placements and
animation frame storage with the full delete-specifier set, file-based
graphics transmission, OSC 52 per-selection slots, the xterm color stack
(XTPUSHCOLORS/XTPOPCOLORS/XTREPORTCOLORS), DECSTR soft reset, selective
erase (DECSCA/DECSED/DECSEL), and a client-side encode_mouse covering
X10/normal/UTF-8/SGR/SGR-pixel.
Graphics payloads are fully decoded in-crate with no dependencies: a
hand-written RFC 1950/1951 inflate (for o=z) and a PNG decoder (8-bit
gray/gray+alpha/RGB/RGBA/indexed, all filters, PLTE/tRNS, CRC-verified)
feed f=100 images and animation frames as RGBA, with frame compositing
(a=c, blend or replace) and composed_frame() for renderers.
Known simplifications: Adam7-interlaced and 16-bit PNGs are rejected; shared-memory graphics transmission answers EUNSUPPORTED (sandbox-safe); OSC 66 text sizing is parsed but scale is not rendered.
The combined CLI. No async runtime; poll()-based event loops like both
originals.
Session persistence (zmx port):
posh attach <name> [command...] # or bare: posh <name>; detach: Ctrl-\
posh list [--short|--json]
posh run <name> [--] <command...>
posh fork [<name>] # fork current session (same cmd + cwd)
posh detach [<name>] | detach-all
posh kill <name>
posh groups
posh history <name> [--vt]
posh completions <bash|zsh|fish>
Attaching takes over the outer terminal's alternate screen (terminfo
smcup/rmcup for $TERM via a built-in term(5) reader, hardcoded 1049 when
no database answers, skipped under --no-init/$POSH_NO_TERM_INIT or for
terminals without an alternate screen) and detaching restores it, so the
shell prompt you attached from comes back exactly as you left it
(FDR 0002: docs/features/). The daemon virtualizes the
session's own alt-screen switches and RIS in the broadcast — replaced
with model-generated repaints — so full-screen apps inside the session
can never flip the outer terminal off posh's screen. Session scrollback is
reached by scrolling the wheel at a bare prompt (FDR 0005) — or as text via
posh history; the outer terminal's native scrollback stays the shell's own.
Daemon-per-session over Unix sockets with zmx's binary IPC framing (1-byte
tag + u32 LE length; Input/Output/Resize/Detach/DetachAll/Kill/Info/Init/
History/Run/Ack/Exit — Exit carries the shell's status so an attached
client exits with the session's real code). Each daemon feeds PTY output
through a posh_term::Terminal
so re-attaching clients receive a full state replay via dump_vt_flat(). Session
groups via -g/--group or POSH_GROUP; socket directory resolution:
POSH_DIR > XDG_RUNTIME_DIR/posh > TMPDIR/posh-{uid} > /tmp/posh-{uid}.
Sessions export POSH_SESSION/POSH_GROUP.
Remote roaming (mosh port) and the unified namespace:
posh [user@]host [-- command] # like mosh(1): plain roaming shell
posh [user@]host:[group/]session # persistent session on the host over
# the roaming transport — attach-or-
# create, detach here, reattach from
# anywhere; [fe80::1]:dev for IPv6
posh host:[group/]session --detach # create a detached session on the host
# and return (remote attach --detach)
posh :[group/]session # explicit local attach
posh -g GROUP list host: # remote listing (host-prefixed names);
# -g scopes it to GROUP on the host
posh ssh [-4|-6] [-p RANGE] [user@]host [-- command]
posh server [new] [-p PORT[:PORT2]] [-4|-6] [-- command...]
posh client [-4|-6] <host> <port> # key via POSH_KEY, never on argv
The grammar is scp-style and total (RFC 0001: docs/rfcs/); every
pre-namespace form keeps its meaning. The remote session's exit status
propagates: posh box:dev; echo $? reports the session shell's code.
Tailscale peers are first-class hosts: shell completion offers tailnet
peer names (MagicDNS) alongside ~/.ssh/config aliases and session names
(posh tailnet lists them), and the roaming transport falls back to a
peer's tailnet IP when the system resolver can't reach its MagicDNS name.
Both degrade silently without tailscale.
The ssh bootstrap runs posh-server new on the remote host (mosh-server
parity); the package installs posh-server as an alias of posh, so the
server host just needs the package on its non-interactive PATH.
Encrypted UDP datagrams using AES-128-GCM with mosh's nonce layout
(direction bit + 63-bit sequence number), replay protection with a reorder
window, timestamp echo for RFC 6298 RTT estimation, fragmentation for large
frames, and server-side roaming by adopting the source address of the newest
authenticated datagram (late reorders never re-target the stream). State sync sends complete dump_vt() frames (or a
prefix/suffix diff against the last acked frame); a client that advertises
SCROLLBACK also accumulates the primary-screen scrollback incrementally
(append-only rows, per-frame cost bounded by inter-frame growth rather than
ring depth — RFC 0002); user input is delivered reliably via cumulative
offsets and retransmission.
The client takes over the alternate screen for the whole connection
(mosh's smcup/rmcup) and restores the pre-connect shell screen on exit
and around suspend. It renders mosh-style: it maintains a local posh_term::Terminal,
morphs the real tty with minimal per-cell diffs (a port of
terminaldisplay.cc), and runs a faithful port of mosh's prediction engine
(terminaloverlay.cc): speculative local echo with epochs, confirmation
against server echo-acks, adaptive display with mosh's SRTT/glitch/flagging
constants, and underlined predictions when the link is slow
(POSH_PREDICTION_MODEL: always/never/adaptive/experimental). POSH_PREDICTION_MODEL=optimistic
instead echoes keystrokes immediately and lets the next paint correct them, gated
on the remote PTY's ECHO flag and alt-screen (FDR 0006). The prediction model and
the render style are independent, swappable axes: POSH_PREDICTION_RENDER selects
how predicted cells are painted (replace, the default, or dim). POSH_PREDICTION
is a deprecated alias for POSH_PREDICTION_MODEL. A reverse-video
"Last contact N seconds ago" banner appears after 6.5s of silence. Ctrl-^ opens
a command palette — the escape menu — listing the predictive-echo models, the
debug-logging toggle, escape-to-shell on the server, suspend, and quit (rendered
by the posh-palette helper); Ctrl-^ then . is a fallback quit if that helper
can't launch. Servers
bind dual-stack IPv6 when possible,
report POSH IP from $SSH_CONNECTION for the ssh wrapper, require UTF-8
locales on both ends (forwarding LANG/LC_* over ssh), forward TERM and
COLORTERM so the session isn't left color-blind (TERM resolved to an entry
the remote's terminfo database actually has), and honor
POSH_SERVER_NETWORK_TMOUT / POSH_SERVER_SIGNAL_TMOUT.
The renderer also ports mosh's scroll optimization (matched rows are
scrolled with \r\n runs or a DECSTBM region instead of being rewritten)
and emits OSC 8 hyperlinks.
Because the whole connection lives on the outer terminal's alternate screen,
the mouse wheel at a bare prompt is at the mercy of that terminal's
alt-screen wheel behavior — kitty, for one, turns it into arrow keys and
ignores DECSET 1007 (posh#3/#28). posh grabs the wheel (mouse reporting) at
a bare prompt and, by default, scrolls the session's own primary-screen
scrollback locally from the client's accumulated ring — instant, no
round-trip, frozen while scrolled, with a top status-bar indicator; any
keystroke or scrolling back to the bottom resumes the live view (FDR 0005,
posh#43). POSH_GRAB_MOUSE=on reverts the wheel to the legacy transform
(wheel up/down → arrow keys, normalizing scroll across terminals, posh#50)
instead of scrollback. Either way, grabbing the wheel costs the outer
terminal's click-to-select while active; a session app that tracks the mouse
itself (vim, tmux) keeps the wheel.
Known simplifications relative to mosh: frames carry dump_vt() state (or
a prefix/suffix diff) rather than mosh's SSP protobuf instructions with
zlib; no utmp/motd integration. The full parity contract — what is
mirrored, what is deliberately dropped, and the open gaps — is FDR 0003
(docs/features/), with the living checklist in issue #44.
A deterministic terminal recorder/replayer built on posh-term: replay a
recorded output byte stream through the in-process emulator and inspect the
exact screen, with no live terminal and no timing to race (the
tmux capture-pane + sleep flake that motivates it). Depends only on
posh-term; surfaced as the standalone poshterity binary and as posh rec.
poshterity record [--out f.castx] -- <cmd> # record a command under a PTY
posh --record f.castx <session> # record a live posh session
poshterity replay <file> [--dump text|vt|flat] # or: posh rec replay ...
poshterity step <file> --by change --n 3 # step-debug, dump each screen
poshterity bless <file> --golden g --at K # write a golden-frame snapshot
poshterity assert <file> --golden g --at K # check it (CI gate)
The recording format is .castx, a strict superset of asciinema .cast v2
(standard o/i/r events plus an ignorable m marker and a poshterity
header block), so any .cast replays through poshterity and any .castx plays
in asciinema. step advances by an emulator-defined granularity
(byte/escape/write/change/frame/marker) and dumps the intermediate
screen — a deterministic VT100 frame debugger. bless/assert snapshot the
screen at a marker (grid is diff-friendly text + a style sidecar; vt is the
raw escape stream) — the deterministic analog of tmux capture-pane; a library
of typed assertion helpers (find_line, cells_have_fg/bg, cells_are_*)
renders a colored expected-vs-actual diff on mismatch. Issue #56 tracks the
epic; adoption + the .castx RFC land in the final phase.
nix build # the full toolset: posh, posh-server, poshterity, posht (#73)
nix build .#posh # hermetic build + cargo test (posh workspace; mosh-ffi gated separately)
just build-rust # same, via the justfile lane
just debug-cargo test --workspace # fast in-worktree dev-loop (includes mosh-ffi)
nix run .#poshterity -- ... # the recorder/replayer as a standalone tool
nix build .#checks.<sys>.mosh-ffi # the C++ FFI oracle gate (just test-mosh-ffi)
nix build .#posht # the interactive capability test (just build-go;
# part of `just build`/`test`). See docs/posht.md.
The workspace builds warning-free and carries ~400 tests (parser state machine, UTF-8 and wide-char edge cases, reflow, SGR colon forms, kitty keyboard encode vectors, graphics ACK paths, inflate and PNG decode vectors, frame compositing, dump_vt roundtrips, IPC framing, crypto seal/open/replay/tamper, fragmentation, RTT, prediction engine state transitions with injected clocks, display-diff and scroll-optimization morphing roundtrips, IPv6 loopback, and daemon lifecycle integration tests).