repos / zmx

session persistence for terminal processes
git clone https://github.com/neurosnap/zmx.git

commit
932e0ed
parent
3b0e4e8
author
Eric Bower
date
2026-03-23 11:21:28 -0400 EDT
docs: cleanup and rewording

I'm removing `AGENTS.md` mainly because it is not particularly useful and there is research to
suggest these files don't really improve agent performance.
4 files changed,  +30, -74
D AGENTS.md
+0, -47
 1@@ -1,47 +0,0 @@
 2-# zmx
 3-
 4-The goal of this project is to create a way to attach and detach terminal sessions without killing the underlying linux process.
 5-
 6-When researching `zmx`, also read the @README.md in the root of this project directory to learn more about the features, documentation, prior art, etc.
 7-
 8-## tech stack
 9-
10-- `zig` v0.15.1
11-- `libghostty-vt` for terminal escape codes and terminal state management
12-
13-## commands
14-
15-- **Build:** `zig build`
16-- **Build Check (Zig)**: `zig build check`
17-- **Test (Zig):** `zig build test`
18-- **Test filter (Zig)**: `zig build test -Dtest-filter=<test name>`
19-- **Formatting (Zig)**: `zig fmt .`
20-
21-## find any library API definitions
22-
23-Before trying anything else, run the `zigdoc` command to find an API with documentation:
24-
25-```
26-zigdoc {symbol}
27-# examples
28-zigdoc ghostty-vt
29-zigdoc std.ArrayList
30-zigdoc std.mem.Allocator
31-zigdoc std.http.Server
32-```
33-
34-Only if that doesn't work should you grep the project dir.
35-
36-## find zig std library source code
37-
38-To inspect the source code for zig's standard library, look inside the `zig_std_src` folder.
39-
40-## find ghostty library source code
41-
42-To inspect the source code for zig's standard library, look inside the `ghostty_src` folder.
43-
44-## Issue Tracking
45-
46-We use bd (beads, https://github.com/steveyegge/beads) for issue tracking instead of Markdown TODOs or external tools.
47-
48-Run `bd quickstart` to learn how to use it.
M CHANGELOG.md
+7, -0
 1@@ -2,6 +2,13 @@
 2 
 3 Use spec: https://common-changelog.org/
 4 
 5+## Staged
 6+
 7+### Fixed
 8+
 9+- `zmx list` will send "no sessions found" to stderr instead of stdout
10+- `zmx wait` will send errors to stderr instead of stdout
11+
12 ## v0.4.2 - 2026-03-18
13 
14 ### Changed
M README.md
+13, -21
 1@@ -15,7 +15,7 @@
 2 
 3 ## features
 4 
 5-- Persist terminal shell sessions (pty processes)
 6+- Persist terminal shell sessions
 7 - Ability to attach and detach from a shell session without killing it
 8 - Native terminal scrollback
 9 - Multiple clients can connect to the same session
10@@ -73,10 +73,10 @@ Commands:
11   [r]un <name> [command...]      Send command without attaching, creating session if needed
12   [d]etach                       Detach all clients from current session  (ctrl+\ for current client)
13   [l]ist [--short]               List active sessions
14-  [c]ompletions <shell>          Completion scripts for shell integration (bash, zsh, or fish)
15   [k]ill <name>                  Kill a session and all attached clients
16   [hi]story <name> [--vt|--html] Output session scrollback (--vt or --html for escape sequences)
17   [w]ait <name>...               Wait for session tasks to complete
18+  [c]ompletions <shell>          Completion scripts for shell integration (bash, zsh, or fish)
19   [v]ersion                      Show version information
20   [h]elp                         Show this help message
21 ```
22@@ -313,6 +313,8 @@ Host = d.*
23     ControlPersist 10m
24 ```
25 
26+Architecturally, `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!
27+
28 Now you can spawn as many terminal sessions as you'd like:
29 
30 ```bash
31@@ -322,9 +324,9 @@ ssh d.pico
32 ssh d.dotfiles
33 ```
34 
35-This will create or attach to each session and since we are using `ControlMaster` the same `ssh` connection is reused for every call to `ssh` for near-instant connection times.
36+Because the `attach` command is essentially an "upsert", this will create or attach to each session.
37 
38-Now 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 laptop lid it will automatically reconnect all your ssh connections:
39+Now 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:
40 
41 ```bash
42 autossh -M 0 -q d.term
43@@ -345,6 +347,8 @@ ash d.dotifles
44 
45 Wow! 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.
46 
47+The end-game here would be to leverage your window manager's ability to automatically arrange your windows for each project with a single command.
48+
49 ## socket file location
50 
51 Each session gets its own unix socket file. The default location depends on your environment variables (checked in priority order):
52@@ -373,7 +377,7 @@ We are evaluating what should be configurable and what should not. Every configu
53 ## known issues
54 
55 - 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
56-- Terminal state rehydration with nested `zmx` sessions through SSH: host A `zmx` -> SSH -> host B `zmx`
57+- Terminal state restoration with nested `zmx` sessions through SSH: host A `zmx` -> SSH -> host B `zmx`
58   - Specifically cursor position gets corrupted
59 - When re-attaching and kitty keyboard mode was previously enable, we try to re-send that CSI query to re-enable it
60   - Some programs don't know how to handle that CSI query (e.g. `psql`) so when you type it echos kitty escape sequences erroneously
61@@ -404,31 +408,19 @@ In this way, `ghostty-vt` doesn't sit in the middle of an active terminal sessio
62 
63 ## prior art
64 
65-Below is a list of projects that inspired me to build this project.
66+Below 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.
67 
68 ### shpool
69 
70-You can find the source code at this repo: https://github.com/shell-pool/shpool
71+https://github.com/shell-pool/shpool
72 
73 `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.
74 
75-`shpool` can be thought of as a lighter weight alternative to tmux or GNU screen. While tmux and screen take over the whole terminal and provide window splitting and tiling features, `shpool` only provides persistent sessions.
76-
77-The biggest advantage of this approach is that `shpool` does not break native scrollback or copy-paste.
78-
79 ### abduco
80 
81-You can find the source code at this repo: https://github.com/martanne/abduco
82-
83-abduco provides session management i.e. it allows programs to be run independently from its controlling terminal. That is programs can be detached - run in the background - and then later reattached. Together with dvtm it provides a simpler and cleaner alternative to tmux or screen.
84-
85-### dtach
86-
87-You can find the source code at this repo: https://github.com/crigler/dtach
88-
89-A simple program that emulates the detach feature of screen.
90+https://github.com/martanne/abduco
91 
92-dtach is a program written in C that emulates the detach feature of screen, which allows a program to be executed in an environment that is protected from the controlling terminal. For instance, the program under the control of dtach would not be affected by the terminal being disconnected for some reason.
93+abduco 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.
94 
95 ## comparison
96 
M src/main.zig
+10, -6
 1@@ -750,6 +750,10 @@ fn wait(cfg: *Cfg, session_names: std.ArrayList([]const u8)) !void {
 2     var stdout_writer = std.fs.File.stdout().writer(&stdout_buffer);
 3     const stdout = &stdout_writer.interface;
 4 
 5+    var stderr_buffer: [1024]u8 = undefined;
 6+    var stderr_writer = std.fs.File.stderr().writer(&stderr_buffer);
 7+    const stderr = &stderr_writer.interface;
 8+
 9     // Highest match count seen so far. Lets us distinguish "sessions haven't
10     // appeared yet" (keep polling) from "sessions we were tracking
11     // disappeared" (fail -- daemon crashed or was killed).
12@@ -780,8 +784,8 @@ fn wait(cfg: *Cfg, session_names: std.ArrayList([]const u8)) !void {
13                 // is no longer deleted, so this session would otherwise
14                 // persist as task_ended_at==0 forever → infinite "still
15                 // waiting". Count it as done+failed so wait terminates.
16-                try stdout.print("task unreachable: {s} ({s})\n", .{ session.name, session.error_name orelse "unknown" });
17-                try stdout.flush();
18+                try stderr.print("task unreachable: {s} ({s})\n", .{ session.name, session.error_name orelse "unknown" });
19+                try stderr.flush();
20                 agg_exit_code = 1;
21                 done += 1;
22                 continue;
23@@ -806,8 +810,8 @@ fn wait(cfg: *Cfg, session_names: std.ArrayList([]const u8)) !void {
24         // crashed and the remaining N-1 happen to be done, total==done
25         // would be a false success.
26         if (total < max_seen) {
27-            try stdout.print("error: {d} session(s) disappeared before completing\n", .{max_seen - total});
28-            try stdout.flush();
29+            try stderr.print("error: {d} session(s) disappeared before completing\n", .{max_seen - total});
30+            try stderr.flush();
31             std.process.exit(1);
32             return;
33         }
34@@ -827,8 +831,8 @@ fn wait(cfg: *Cfg, session_names: std.ArrayList([]const u8)) !void {
35             // typo, not a slow start.
36             zero_match_iters += 1;
37             if (zero_match_iters >= 3) {
38-                try stdout.print("error: no matching sessions found\n", .{});
39-                try stdout.flush();
40+                try stderr.print("error: no matching sessions found\n", .{});
41+                try stderr.flush();
42                 std.process.exit(2);
43                 return;
44             }