1<h1>
2<p align="center">
3 <img src="./logo.png" alt="Logo" width="128">
4 <br>zmx
5</h1>
6<p align="center">
7 Session persistence for terminal processes.
8 <br />
9 <a href="https://zmx.sh">Docs</a>
10 ·
11 <a href="https://bower.sh/you-might-not-need-tmux">You might not need tmux</a>
12 ·
13 Sponsored by <a href="https://pico.sh">pico.sh</a>
14</p>
15
16## features
17
18- Persist terminal shell sessions
19- Ability to attach and detach from a shell session without killing it
20- Native terminal scrollback
21- Multiple clients can connect to the same session
22- Re-attaching to a session restores previous terminal state and output
23- Send commands to a session without attaching to it
24- Print scrollback history of a terminal session in plain text
25- Works on mac and linux
26- This project does **NOT** provide windows, tabs, or splits
27
28## demos
29
30- [zmx - intro](https://youtu.be/UIXj0_rhPgI)
31- [zmx - ai portal](https://youtu.be/CV3skPYHP4Q)
32
33## install
34
35### binaries
36
37- https://zmx.sh/a/zmx-0.5.0-linux-aarch64.tar.gz
38- https://zmx.sh/a/zmx-0.5.0-linux-x86_64.tar.gz
39- https://zmx.sh/a/zmx-0.5.0-macos-aarch64.tar.gz
40- https://zmx.sh/a/zmx-0.5.0-macos-x86_64.tar.gz
41
42### homebrew
43
44```bash
45brew install neurosnap/tap/zmx
46```
47
48### packages (unofficial)
49
50- [Alpine Linux](https://pkgs.alpinelinux.org/package/edge/testing/x86_64/zmx)
51- [Arch AUR tracking releases](https://aur.archlinux.org/packages/zmx)
52- [Arch AUR tracking git](https://aur.archlinux.org/packages/zmx-git)
53- [openSUSE Tumbleweed](https://software.opensuse.org/package/zmx)
54- [Gentoo (overlay)](https://codeberg.org/samuelhautamaki/samuelhautamaki-gentoo)
55- [zmx-rpm packaging](https://github.com/engie/zmx-rpm/)
56
57### src
58
59- Requires zig `v0.15`
60- Clone the repo
61- Run build cmd
62
63Be sure to add `~/.local/bin` to your `PATH`:
64
65```bash
66zig build -Doptimize=ReleaseSafe --prefix ~/.local
67```
68
69## usage
70
71> [!IMPORTANT]
72> We recommend closing the terminal window to detach from the session but you can also press `ctrl+\` or run `zmx detach`.
73
74```
75zmx - session persistence for terminal processes
76
77Usage: zmx <command> [args...]
78
79Commands:
80 [a]ttach <name> [command...] Attach to session, creating if needed
81 [r]un <name> [-d] [--fish] [command...] Send command without attaching
82 [s]end <name> <text...> Send raw input to session PTY
83 [p]rint <name> <text...> Inject text into session display
84 [wr]ite <name> <file_path> Write stdin to file_path through the session
85 [d]etach Detach all clients (ctrl+\\ for current client)
86 [l]ist|ls [--short] List active sessions
87 [k]ill <name>... [--force] Kill session and all attached clients
88 [hi]story <name> [--vt|--html] Output session scrollback
89 [w]ait <name>... Wait for session tasks to complete
90 [t]ail <name>... Follow session output
91 [c]ompletions <shell> Shell completions (bash, zsh, fish)
92 [v]ersion Show version
93 [h]elp Show this help
94
95Attach:
96 This will spawn a login $SHELL with a PTY. You can provide a
97 command instead of creating a shell.
98
99 Examples:
100 zmx attach dev
101 zmx attach dev vim
102
103History:
104 This should generally be used with `tail` to print the last lines
105 of the session's scrollback history.
106
107 Examples:
108 zmx history <session> | tail -100
109
110Run:
111 Commands are passed as-is: do not wrap in quotes.
112 Commands run sequentially: do not send multiple in parallel.
113 Avoid interactive programs (pagers, editors, prompts): they hang.
114
115 `--fish` is required when the session runs fish shell.
116
117 If the command hangs, send Ctrl+C to recover:
118 zmx run <session> $(printf '\x03')
119
120 If the command hangs, print the history to see the error:
121 zmx history <session> | tail -100
122
123 `-d` will detach from the calling terminal. Use `wait` to track
124 its status.
125
126 Examples:
127 zmx run dev ls
128 zmx run dev --fish ls src
129 zmx run dev zig build
130 zmx run dev grep -r TODO src
131 zmx run dev git -c core.pager=cat diff
132
133Send:
134 Sends raw text to the session's PTY input (fire-and-forget).
135 Unlike `run`, no completion marker is appended and no exit code
136 is tracked. Useful for TUI applications, interactive prompts,
137 or any program that reads stdin directly.
138
139 Text is sent byte-for-byte with no automatic carriage return.
140 Append \r yourself when you want the shell to execute a command.
141
142 Text can also be piped via stdin:
143 printf 'ls -la\r' | zmx send dev
144
145 Examples:
146 printf 'echo hello\r' | zmx send dev
147 zmx send dev $(printf '\x03')
148 zmx send dev /compact
149
150Print:
151 Injects text directly into the session display and scrollback.
152 Never touches the PTY input -- the shell sees nothing.
153 Caller is responsible for newlines (\\r\\n).
154
155 Examples:
156 printf '\\r\\nhello\\r\\n' | zmx print dev
157 zmx print dev "$(printf '\\r\\nalert\\r\\n')"
158
159Write:
160 Writes stdin to file_path inside the session. Works over SSH.
161 file_path can be absolute or relative to the session shell's cwd.
162 Requires base64 and printf in the remote environment.
163 Large files are chunked automatically (~48KB per chunk).
164 File path must not contain single quotes.
165
166 Examples:
167 echo "hello" | zmx write dev /tmp/hello.txt
168 cat main.zig | zmx write dev src/main.zig
169
170Wait:
171 Used with a detached run task to track its status. Multiple
172 sessions can be provided.
173
174 Examples:
175 zmx run -d dev sleep 10
176 zmx wait dev
177 zmx wait dev other
178
179Environment variables:
180 SHELL Default shell for new sessions
181 ZMX_DIR Socket directory (priority 1)
182 XDG_RUNTIME_DIR Socket directory (priority 2)
183 TMPDIR Socket directory (priority 3)
184 ZMX_SESSION Session name (injected automatically)
185 ZMX_SESSION_PREFIX Prefix added to all session names
186 ZMX_DIR_MODE Sets mode for socket and log directories (octal, defaults to 0750)
187 ZMX_LOG_MODE Sets mode for log files (octal, defaults to 0640)
188```
189
190## shell prompt
191
192When you attach to a `zmx` session, we don't provide any indication that you are inside `zmx`. We do provide an environment variable `ZMX_SESSION` which contains the session name.
193
194We recommend checking for that env var inside your prompt and displaying some indication there.
195
196### fish
197
198Place this file in `~/.config/fish/config.fish`:
199
200```fish
201functions -c fish_prompt _original_fish_prompt 2>/dev/null
202
203function fish_prompt --description 'Write out the prompt'
204 if set -q ZMX_SESSION
205 echo -n "[$ZMX_SESSION] "
206 end
207 _original_fish_prompt
208end
209```
210
211### bash and zsh
212
213Depending on the shell, place this in either `.bashrc` or `.zshrc`:
214
215```bash
216if [[ -n $ZMX_SESSION ]]; then
217 export PS1="[$ZMX_SESSION] ${PS1}"
218fi
219```
220
221### powerlevel10k zsh theme
222
223[powerlevel10k](https://github.com/romkatv/powerlevel10k) is a theme for zsh that overwrites the default prompt statusline.
224
225Place this in `.zshrc`:
226
227```bash
228function prompt_my_zmx_session() {
229 if [[ -n $ZMX_SESSION ]]; then
230 p10k segment -b '%k' -f '%f' -t "[$ZMX_SESSION]"
231 fi
232}
233POWERLEVEL9K_RIGHT_PROMPT_ELEMENTS+=my_zmx_session
234```
235
236### oh-my-posh
237
238[oh-my-posh](https://ohmyposh.dev) is a popular shell themeing and prompt engine. This code will display an icon and session name as part of the prompt if (and only if) you have zmx active:
239
240```toml
241[[blocks.segments]]
242 template = '{{ if .Env.ZMX_SESSION }} {{ .Env.ZMX_SESSION }}{{ end }}'
243 foreground = 'p:orange'
244 background = 'p:black'
245 type = 'text'
246 style = 'plain'
247```
248
249### Starship
250
251[Starship](https://starship.rs) is a popular shell themeing and prompt engine. This code will display an icon and session name as part of the prompt if (and only if) you have zmx active:
252
253```toml
254format = """
255${env_var.ZMX_SESSION}\
256...
257"""
258
259[env_var.ZMX_SESSION]
260symbol = " "
261format = "[$symbol$env_value]($style) "
262description = "zmx session name"
263style = "bold magenta"
264```
265
266## shell completion
267
268Shell auto-completion for `zmx` commands and session names can be enabled using the `completions` subcommand. Once configured, you'll get auto-complete for both local `zmx` commands and sessions:
269
270```bash
271ssh remote-server zmx attach session-na<TAB>
272# <- auto-complete suggestions appear here
273```
274
275> NOTICE: when installing `zmx` with `homebrew` completions are automatically installed.
276
277### bash
278
279Add this to your `.bashrc` file:
280
281```bash
282if command -v zmx &> /dev/null; then
283 eval "$(zmx completions bash)"
284fi
285```
286
287### zsh
288
289Add this to your `.zshrc` file:
290
291```zsh
292if command -v zmx &> /dev/null; then
293 eval "$(zmx completions zsh)"
294fi
295```
296
297### fish
298
299Add this to `~/.config/fish/completions/zmx.fish`:
300
301```fish
302if type -q zmx
303 zmx completions fish | source
304end
305```
306
307## session picker
308
309You can add an interactive session picker to your shell that lets you fuzzy-find existing sessions, preview their scrollback history, or create new ones -- all from a single prompt. This is especially useful for remote SSH workflows: add it to your shell startup so that connecting to a machine immediately presents the picker.
310
311Requires [fzf](https://github.com/junegunn/fzf).
312
313- **Enter** selects a matched session (or creates one if no sessions exist)
314- **Ctrl-N** creates a new session using the typed query, even when a fuzzy match is highlighted
315
316<details>
317<summary>bash and zsh</summary>
318
319```bash
320zmx-select() {
321 local display
322 display=$(zmx list 2>/dev/null | while IFS=$'\t' read -r name pid clients created dir; do
323 name=${name#session_name=}
324 pid=${pid#pid=}
325 clients=${clients#clients=}
326 dir=${dir#started_in=}
327 printf "%-20s pid:%-8s clients:%-2s %s\n" "$name" "$pid" "$clients" "$dir"
328 done)
329
330 local output query key selected session_name
331 output=$({ [[ -n "$display" ]] && echo "$display"; } | fzf \
332 --print-query \
333 --expect=ctrl-n \
334 --height=80% \
335 --reverse \
336 --prompt="zmx> " \
337 --header="Enter: select | Ctrl-N: create new" \
338 --preview='zmx history {1}' \
339 --preview-window=right:60%:follow \
340 )
341 local rc=$?
342
343 query=$(echo "$output" | sed -n '1p')
344 key=$(echo "$output" | sed -n '2p')
345 selected=$(echo "$output" | sed -n '3p')
346
347 if [[ "$key" == "ctrl-n" && -n "$query" ]]; then
348 session_name="$query"
349 elif [[ $rc -eq 0 && -n "$selected" ]]; then
350 session_name=$(echo "$selected" | awk '{print $1}')
351 elif [[ -n "$query" ]]; then
352 session_name="$query"
353 else
354 return 130
355 fi
356
357 zmx attach "$session_name"
358}
359```
360
361You can call `zmx-select` manually, bind it to a key, or auto-launch it on shell startup when outside a zmx session. With `&& exit`, the normal flow becomes: connect via SSH → pick a session → work → detach or exit the session → SSH disconnects automatically. Cancelling the picker with **Ctrl-C** drops you into a regular shell as an escape hatch.
362
363```bash
364if command -v zmx &> /dev/null && command -v fzf &> /dev/null && [[ -z "$ZMX_SESSION" ]]; then
365 zmx-select && exit
366fi
367```
368
369</details>
370
371## session prefix
372
373We allow users to set an environment variable `ZMX_SESSION_PREFIX` which will prefix the name of the session for all commands. This means if that variable is set, every command that accepts a session will be prefixed with it.
374
375```bash
376export ZMX_SESSION_PREFIX="d."
377zmx a runner # ZMX_SESSION=d.runner
378zmx a tests # ZMX_SESSION=d.tests
379zmx k tests # kills d.tests
380zmx wait # suspends until all tasks prefixed with "d." are complete
381```
382
383## philosophy
384
385The entire argument for `zmx` instead of something like `tmux` that has windows, panes, splits, etc. is that job should be handled by your os window manager. By using something like `tmux` you now have redundant functionality in your dev stack: a window manager for your os and a window manager for your terminal. Further, in order to use modern terminal features, your terminal emulator **and** `tmux` need to have support for them. This holds back the terminal enthusiast community and feature development.
386
387Instead, this tool specifically focuses on session persistence and defers window management to your os wm.
388
389## ssh workflow
390
391Using `zmx` with `ssh` is a first-class citizen. Instead of using `ssh` to remote into your system with a single terminal and `n` tmux panes, you open `n` terminals and run `ssh` for all of them. This might sound tedious, but there are tools to make this a delightful workflow.
392
393First, create an `ssh` config entry for your remote dev server:
394
395```bash
396Host = d.*
397 HostName 192.168.1.xxx
398
399 RemoteCommand zmx attach %k
400 RequestTTY yes
401 ControlPath ~/.ssh/cm-%r@%h:%p
402 ControlMaster auto
403 ControlPersist 10m
404```
405
406Architecturally, `ssh` supports multiplexing multiple channels of communication within a single connection to a server. `ControlMaster` is the setting that tells `ssh` to multiplex multiple PTY sessions to a single server over one tcp connection. Neat!
407
408Now you can spawn as many terminal sessions as you'd like:
409
410```bash
411ssh d.term
412ssh d.irc
413ssh d.pico
414ssh d.dotfiles
415```
416
417Because the `attach` command is essentially an "upsert", this will create or attach to each session.
418
419Now you can use the [`autossh`](https://linux.die.net/man/1/autossh) tool to make your ssh connections auto-reconnect. For example, if you have a laptop and close/open your lid it will automatically reconnect all your ssh connections:
420
421```bash
422autossh -M 0 -q d.term
423```
424
425Or create an `alias`/`abbr`:
426
427```fish
428abbr -a ash "autossh -M 0 -q"
429```
430
431```bash
432ash d.term
433ash d.irc
434ash d.pico
435ash d.dotifles
436```
437
438Wow! Now you can setup all your os tiling windows how you like them for your project and have as many windows as you'd like, almost replicating exactly what `tmux` does but with native windows, tabs, splits, and scrollback! It also has the added benefit of supporting all the terminal features your emulator supports, no longer restricted by what `tmux` supports.
439
440The end-game here would be to leverage your window manager's ability to automatically arrange your windows for each project with a single command.
441
442## socket file location
443
444Each session gets its own unix socket file. The default location depends on your environment variables (checked in priority order):
445
4461. `ZMX_DIR` => uses exact path (e.g., `/custom/path`)
4471. `XDG_RUNTIME_DIR` => uses `{XDG_RUNTIME_DIR}/zmx` (recommended on Linux, typically results in `/run/user/{uid}/zmx`)
4481. `TMPDIR` => uses `{TMPDIR}/zmx-{uid}` (appends uid for multi-user safety)
4491. `/tmp` => uses `/tmp/zmx-{uid}` (default fallback, appends uid for multi-user safety)
450
451## permissions
452
453You can configure the permissions for the socket directory and log files using the following environment variables:
454
455- `ZMX_DIR_MODE` => sets the mode for the socket and log directories (octal, defaults to `0750`)
456- `ZMX_LOG_MODE` => sets the mode for the log files (octal, defaults to `0640`)
457
458This is particularly useful when running `zmx` as a system service with a shared group. For example, setting `ZMX_DIR_MODE=0770` and `ZMX_LOG_MODE=0660` allows group members to attach to the session.
459
460## debugging
461
462We store global logs for cli commands in `{socket_dir}/logs/zmx.log`. We store session-specific logs in `{socket_dir}/logs/{session_name}.log`. Right now they are enabled by default and cannot be disabled. The idea here is to help with initial development until we reach a stable state.
463
464## a note on configuration
465
466We are evaluating what should be configurable and what should not. Every configuration option is a burden for us maintainers. For example, being able to change the default detach shortcut is difficult in a terminal environment.
467
468## a smol contract
469
470- Write programs that solve a well defined problem.
471- Write programs that behave the way most users expect them to behave.
472- Write programs that a single person can maintain.
473- Write programs that compose with other smol tools.
474- Write programs that can be finished.
475
476## known issues
477
478- When upgrading versions of `zmx` where we make changes to the underlying IPC communication, it will kill all your sessions because it cannot communicate through the daemon socket properly
479- Terminal state restoration with nested `zmx` sessions through SSH: host A `zmx` -> SSH -> host B `zmx`
480 - Specifically cursor position gets corrupted
481- When re-attaching and kitty keyboard mode was previously enable, we try to re-send that CSI query to re-enable it
482 - Some programs don't know how to handle that CSI query (e.g. `psql`) so when you type it echos kitty escape sequences erroneously
483
484## impl
485
486- The `daemon` and client processes communicate via a unix socket
487- Both `daemon` and `client` loops leverage `poll()`
488- Each session creates its own unix socket file
489- We restore terminal state and output using `libghostty-vt`
490
491### libghostty-vt
492
493We use `libghostty-vt` to restore the previous state of the terminal when a client re-attaches to a session.
494
495How it works:
496
497- user creates session `zmx attach term`
498- user interacts with terminal stdin
499- stdin gets sent to pty via daemon
500- daemon sends pty output to client *and* `ghostty-vt`
501- `ghostty-vt` holds terminal state and scrollback
502- user disconnects
503- user re-attaches to session
504- `ghostty-vt` sends terminal snapshot to client stdout
505
506In this way, `ghostty-vt` doesn't sit in the middle of an active terminal session, it simply receives all the same data the client receives so it can re-hydrate clients that connect to the session. This enables users to pick up where they left off as if they didn't disconnect from the terminal session at all. It also has the added benefit of being very fast, the only thing sitting in-between you and your PTY is a unix socket.
507
508## prior art
509
510Below is a list of projects that inspired me to build this project. Architecturally, `zmx` uses aspects of both projects. For example, `shpool` inspired the idea of having libghostty restore the terminal state on reattach. Abduco inspired the idea of one daemon (and unix socket) per session.
511
512### shpool
513
514https://github.com/shell-pool/shpool
515
516`shpool` is a service that enables session persistence by allowing the creation of named shell sessions owned by `shpool` so that the session is not lost if the connection drops.
517
518### abduco
519
520https://github.com/martanne/abduco
521
522abduco provides session management (i.e. it allows programs to be run independently from its controlling terminal). Together with dvtm it provides a simpler alternative to tmux or screen.
523
524## comparison
525
526| Feature | zmx | shpool | abduco | dtach | tmux |
527| ------------------------------ | --- | ------ | ------ | ----- | ---- |
528| 1:1 Terminal emulator features | ✓ | ✓ | ✓ | ✓ | ✗ |
529| Terminal state restore | ✓ | ✓ | ✗ | ✗ | ✓ |
530| Window management | ✗ | ✗ | ✗ | ✗ | ✓ |
531| Multiple clients per session | ✓ | ✗ | ✓ | ✓ | ✓ |
532| Native scrollback | ✓ | ✓ | ✓ | ✓ | ✗ |
533| Configurable detach key | ✗ | ✓ | ✓ | ✓ | ✓ |
534| Auto-daemonize | ✓ | ✓ | ✓ | ✓ | ✓ |
535| Daemon per session | ✓ | ✗ | ✓ | ✓ | ✗ |
536| Session listing | ✓ | ✓ | ✓ | ✗ | ✓ |
537
538## community tools
539
540- [pi-zmx](https://github.com/deevus/pi-zmx) -- [pi](https://pi.dev) extension for zmx.
541- [zsm](https://github.com/mdsakalu/zmx-session-manager) -- TUI session manager for zmx. List, preview, filter, and kill sessions from an interactive terminal UI.
542- [zmosh](https://github.com/mmonad/zmosh) -- A fork of zmx that adds encrypted UDP auto-reconnect for remote sessions (like mosh).