(345) commits
Eric Bower
·
2026-04-29
feat(wait): summary of failed tasks and actionable commands to run This simply prints the failed tasks and shows some commands to run to see what went wrong.
Eric Bower
·
2026-04-28
refactor(attach): reset outer terminal to default state Closes: https://github.com/neurosnap/zmx/issues/106
Eric Bower
·
2026-04-26
feat: cross-compile from linux to mac Latest version of libghostty now supports cross-compiling without needing Apple sdk tools.
Radosław Stachowiak
·
2026-04-26
feat: accept ls as list alias to help our muscle memory
Ian Tay
·
2026-04-10
refactor(ipc): freeze Tag wire values; switch via connectSession
Ian Tay
·
2026-04-10
fix(daemon): self-pipe signal wakeup + version-tolerant IPC liveness
Self-pipe (idle daemon was deaf to SIGTERM/SIGWINCH):
std.posix.poll loops on .INTR internally (PollError has no Interrupted
member), so the prior `catch error.Interrupted` was unreachable since
8640be51/690487b — signals only "worked" when unrelated I/O woke poll().
Replace with the self-pipe trick: posix.pipe2(.{CLOEXEC,NONBLOCK}), one
wakeSignalPipe handler with errno save/restore, pipe read-end at fixed
poll_fds[2] in both loops; resize/shutdown act in the drain branch.
Removes the now-redundant sigwinch/sigterm atomic flags and collapses
setupSig*Handler into installWakeHandler(sig).
IPC versioning (Option E — decouple liveness from Info shape):
connectSession() does connect-only liveness; 5 of 6 callers (kill,
detach, history, run, attach) switch to it so they survive Info struct
changes. probeSession keeps the Info round-trip for `list`, which now
reports InfoSizeMismatch instead of Unexpected on version skew.
Hygiene: clients_len usize->u64 (extern struct); handleInfo zero-inits
Info so asBytes() doesn't ship 7B tail padding + cmd/cwd stack tails.
Tests freeze @sizeOf(Info)=552 / @sizeOf(Header)=8 and assert zeroed
Info ships zero padding; wired via test{_=ipc;}.
Eric Bower
·
2026-04-25
fix(run): ignore DA queries when no terminal client connected `run` is not a real terminal client, it just tails the output and cannot respond to shell QA queries so we have a special flag that ignores read-only clients
Eric Bower
·
2026-04-24
refactor(run): check pty foreground process to detect shell This removes the need to provide the `--fish` flag at all for `zmx run` commands.
Eric Bower
·
2026-04-23
feat: print cmd This sends text directly to client's stdout as-is. Typically you'll want to wrap the text in '\r\n' but we leave that up to the end-user
Pavel Borzenkov
·
2026-04-24
fix(daemon): reset leader fd on client disconnect (#141)
Make sure leader fd is reset when the leader client disconnects abruptly
without going through 'Detach' IPC. Otherwise, the daemon may remember
wrong client fd as the leader and run all input from the client via
libghostty parser.
Fixes #135
NOTE: this currently doesn't address the issue when libghostty can't
parse the byte sequence and complains like this:
[warning] (parser): CSI colon or mixed separators only allowed for 'm'
command, got: terminal.Parser.Action{ .csi_dispatch = .{ .intermediates
= { }, .params = { 57444, 1, 1 }, .params_sep = .{ .mask = 2 }, .final
= 117 } }
Jay Botte
·
2026-04-21
feat: send raw bytes to pty input This is a wrapper around the `.Input` event. Send delivers bytes to the PTY exactly as-is with no automatic carriage return. The caller is responsible for appending \r when shell command submission is desired.
x1f9
·
2026-04-02
test: add BATS session lifecycle tests and mise.toml 14 tests covering session creation, listing, killing, history, wait, ZMX_DIR isolation, and rapid churn. These tests create real daemon processes — without the inherited-FD close fix, every test that calls `zmx run` would hang indefinitely. mise.toml declares bats 1.13.0 as a dev dependency, establishing the pattern for managing project tooling.
x1f9
·
2026-04-02
fix(daemon): redirect stdio to /dev/null after fork The existing FD close loop (3-63) handles bats' internal file descriptors, but bats' `run` keyword captures output via pipes on FDs 0-2. The daemon inherits these pipe write ends and holds them open, so bats never gets EOF and hangs. Redirect stdin/stdout/stderr to /dev/null right after setsid(). The daemon communicates exclusively via its unix socket — it never reads stdin or writes stdout/stderr. This is standard daemon hygiene and completes the inherited-FD fix.
ikma
·
2026-03-28
fix(daemon): close inherited file descriptors after fork After the daemon forks and re-initializes its own log file, close all file descriptors from 3 to 63 (excluding server_sock_fd). These FDs are inherited from the parent process and are not needed by the daemon. Without this fix, test harnesses like bats hang indefinitely. Bats uses FDs 3+ internally and waits for them to close before exiting. Since zmx forks a long-lived daemon that inherits these FDs, bats never sees them close and blocks forever. The close happens after log_system.deinit()/init() to avoid closing the parent's log FD before it's properly released, and before spawnPty() so the PTY FD (allocated after this point) is not affected. Uses std.c.close() (raw libc) instead of std.posix.close() to avoid panicking on already-closed or invalid FDs.
Eric Bower
·
2026-04-16
fix(write): remove bracketed paste Some shell environments don't support it and we are only sending 1 line at a time so we should be okay to remove it.
Eric Bower
·
2026-04-14
refactor: require "*" suffix for wildcard matches This applies to: wait, kill, and tail Examples: - zmx wait "dev*" - zmx kill "dev*" - zmx tail "dev*" - zmx kill "*"
Eric Bower
·
2026-04-10
feat: tail, write, and run -d I'm calling this the ai portal integration. These features allow a local code agent to be fully operational against a zmx session that is remote. Everything works by sending key strokes directly into the zmx session. This means it doesn't matter where the remote session lives it should work as if you were typing the commands yourself. SSH'd into a server? Inside a container? It's all the same to zmx and the code agent. BREAKING CHANGE: `zmx run` is now synchronous by default *and* immediately tails the session output so the agent can get immediate feedback. To use the previous run behavior where the run command completes immediately use detached mode: `zmx run -d`
Shravan Sunder
·
2026-04-14
fix: rewrite OSC 133;A with redraw=0 to prevent prompt loss on resize (#112) When zmx forwards shell output containing OSC 133;A (prompt start) to the outer terminal, the terminal sets shell_redraws_prompt=true. On resize, the terminal clears prompt rows expecting the shell to redraw them. But the shell's redraw goes through zmx's IPC relay chain with cursor coordinates relative to the inner PTY state, causing a cursor desync that makes the prompt invisible. Fix: rewrite OSC 133;A to include redraw=0 before forwarding. This tells the outer terminal that the process on its PTY (zmx client) cannot redraw prompts, so it should leave prompt rows intact on resize. This is a standard Kitty protocol extension. Also disables shell_redraws_prompt on the daemon's internal ghostty_vt terminal during resize to prevent snapshot state corruption. Fixes #111. Likely also fixes #99.
Eric Bower
·
2026-04-12
chore: dont show task_command because it is unreliable We just don't have a great way to send [][]u8 over ipc because we are using structs with fixed size fields that are converted to/from via std.mem.asBytes. This means we have to convert the command from [][]u8 to []u8 but that makes our daemon struct fields complicated. Instead, just rely on looking at the zmx history to understand the command that was run.
Adrian
·
2026-04-13
feat: allow configuring socket and log permissions via environment variables (#130)
Eric Bower
·
2026-04-09
refactor: set leader by detecting user input with libghostty parser
Michael Sakaluk
·
2026-03-18
fix: two-phase terminal state serialization for nested session cursor corruption Fixes #31 (cursor position corruption on re-attach over SSH). Partially addresses #86 (cursor style error after attach). Contributes to #96 (testing framework). Problem: When running nested zmx sessions (zmx→SSH→zmx), re-attaching to the outer session causes cursor positions and screen content to appear at wrong rows. The root cause is that serializeTerminalState() writes scrollback + visible content sequentially. Scrollback lines scroll the terminal before CUP sequences arrive, shifting all visible content by the scrollback overflow. Fix: Split serialization into two phases: 1. Phase 1: Emit scrollback content only (no modes/cursor/keyboard). These lines scroll into the terminal's scrollback buffer. 2. Clear visible screen (ESC[2J ESC[H ESC[0m) to reset for phase 2. 3. Phase 2: Emit visible screen only with full extras (modes, cursor, keyboard, scrolling region). The clear between phases ensures visible content starts from a clean slate regardless of how much scrollback preceded it. Scrollback is preserved in the terminal's buffer for native scroll-up. This approach is inspired by tmux's visible-only redraw strategy but preserves terminal scrollback. Tests: Added 7 integration tests using ghostty-vt roundtrip verification: - Cursor position preservation after roundtrip - CUP-positioned markers survive roundtrip - Scrollback does not shift visible content (the core bug) - Nested double-roundtrip (inner→outer→client) - Alternate screen content not leaked - Terminal size mismatch (30→24 rows) + roundtrip - Scrollback + size mismatch + nested roundtrip (stress test) All tests run in <1 second via `zig build test`.
Eric Bower
·
2026-04-03
feat(attach): support switching sessions Previously we did not allow users to switch to a session from within a session via `zmx attach`. Now users are able to run `zmx attach` from within a session and it'll properly detach and then reattach to the new session. References: https://github.com/neurosnap/zmx/issues/91
Eric Bower
·
2026-04-08
feat: change pty window size when new leader is set When we set a new client leader we send a Resize event request from the daemon to the client. So now whenever a user types into their stdin we set the new leader and then resize the window.
Eric Bower
·
2026-04-04
refactor: kitty key press logic to be more generic fix: src/util.zig tests were not being run :sad:
Eric Bower
·
2026-04-03
feat: new client leader policy The last client to send user input bytes (non-ansi escape codes) becomes the leader. The client leader controls resizing and any other terminal state changes. Non-leader clients are read-only until they send user input bytes and takeover leadership. Closes: https://github.com/neurosnap/zmx/issues/73
Eric Bower
·
2026-04-02
feat(kill): add `--force` flag to remove the socket file If we cannot communicate with the daemon then it could mean a number of things. We don't automatically assume that a communication error means we can never connect to it. So we add a `--force` command for when there is an error the user cannot recover from and we will delete the socket file. Unfortunately, we do not know the pid for the terminal session outside of the daemon so we cannot automatically kill the pid in some cases which means it could linger.
とーふとふ
·
2026-03-31
fix(daemon): close pty master before waitpid to prevent zombie on macOS (#114)
Ian Tay
·
2026-03-18
feat(daemon): buffer PTY stdin writes and flush via POLLOUT Replaces the best-effort ptyWrite (drop on EAGAIN) with a buffered queue flushed by the daemon's poll loop. Mirrors the pattern already used for client-socket writes. - pty_write_buf on Daemon, capped at 256KB (drop new payload on overflow — same failure mode as before, 64x higher threshold) - POLLOUT registered on pty_fd when buffer non-empty; flush handler loops until EAGAIN - handleInput/handleRun queue instead of writing directly - respondToDeviceAttributes routes through the same buffer so DA responses can't interleave with a draining .Run payload after client disconnect Follow-up to #82.
Eric Bower
·
2026-03-23
feat(kill): accepts multiple args and matches session prefixes Examples: - zmx kill one - zmx kill one two three - zmx kill d. - ZMX_SESSION_PREFIX="d." zmx kill
Eric Bower
·
2026-03-23
chore: dockerfile and ci file I'm experimenting with using zmx as a job engine for CI.
Eric Bower
·
2026-03-23
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.
Eric Bower
·
2026-03-22
fix(list): send no session found msg to stderr Fixes: https://github.com/neurosnap/zmx/issues/101
Patrik Sundberg
·
2026-03-17
fix(attach): reject combined intentional modifiers for Ctrl+\ detach The parser accepted any modifier combination that included ctrl (ctrl+shift, ctrl+alt, ctrl+super, etc.) because it only checked the ctrl bit. This was over-permissive: ctrl+shift+\ is a different key combination and should not trigger detach. Tighten the check to require ctrl as the ONLY intentional modifier. Lock modifiers (caps_lock, num_lock) remain tolerated since they are ambient state, not deliberate key combinations.
Patrik Sundberg
·
2026-03-15
fix(attach): handle all kitty keyboard protocol encodings for Ctrl+\ The old detection used rigid substring matching for exactly two sequences (\x1b[92;5u and \x1b[92;5:1u). This failed when apps enabled progressive enhancement flags that change the encoding: - Lock modifiers (caps_lock, num_lock) alter the modifier value - Alternate key sub-fields add :shifted:base after the key code - Combined modifiers (ctrl+shift, ctrl+alt, etc.) change the value - Text codepoint sections append an extra ;codepoints field Replace with a proper CSI u parser that: 1. Matches key code 92 (backslash) with any alternate sub-fields 2. Checks the ctrl bit in the modifier value: (mod - 1) & 0b100 3. Accepts press/repeat events, rejects release 4. Tolerates optional text codepoint sections
Michael Sakaluk
·
2026-03-10
fix: validate session name length against Unix socket path limit Session names that cause the socket path to exceed the OS sun_path limit (104 bytes on macOS, 108 on Linux) now produce a clear error message instead of failing silently with exit code 1. - Add max_socket_path_len constant derived from platform sockaddr_un - Add validation in getSocketPath returning error.NameTooLong - Add maxSessionNameLen helper for dynamic limit computation - Add printSessionNameTooLong for user-facing error on stderr - Handle NameTooLong in all command paths (run, attach, kill, history, detach, list) - Move validation before sessionExists in kill/history so the error message is shown even when the socket file doesn't exist - Add 6 unit tests covering boundary conditions and platform constants Fixes: https://github.com/neurosnap/zmx/issues/84
Iván Hernández Cazorla
·
2026-03-12
chore: add openSUSE package link to website (#89)
Michael Sakaluk
·
2026-03-12
fix: print error to stderr when session does not exist (#88)
Eric Bower
·
2026-03-11
chore(history): bare `zmx history` should try to use `ZMX_SESSION`
Ian Tay
·
2026-03-08
fix(ipc): validate wire data before arithmetic and indexing - expectedLength: header.len is u32 off the wire; adding sizeof(Header) at u32 width could wrap (panic in safe mode, UB slice bounds in release). Widen to usize first. - get_session_entries: cmd_len/cwd_len are u16 (max 65535) but index into [256]u8 arrays. Clamp before slicing.
Ian Tay
·
2026-03-08
fix(attach): skip termios setup when stdin is not a TTY tcgetattr's return value was discarded, so when stdin was piped (e.g. `zmx attach foo < /dev/null`), orig_termios remained Zig `undefined` stack bytes. These garbage bytes were then copied, modified by cfmakeraw, and applied via tcsetattr — UB in safe builds.
Ian Tay
·
2026-03-08
fix(attach): serialize terminal state before resize, as the comment says The existing comment correctly explains why serialize must happen before resize (reflow moves the cursor), but the code did the opposite. Reattaching with a different terminal size could leave the cursor misplaced.
Ian Tay
·
2026-03-08
fix(pty): isolate forked child from parent code path and heap-alloc argv Two issues in spawnPty's child (pid==0) branch: 1. `try` on allocPrint/bufPrintZ could propagate an error past the if-block, causing the forked child to fall through to the parent code path — running a second daemon on the same socket, or hitting errdefers that delete the parent's socket file. The bufPrintZ case is triggerable via a long SHELL basename. 2. The fixed-size argv_buf[64] overflowed with >63 CLI arguments. Both are fixed by extracting child setup into execChild() which returns !noreturn (exec or error), with the caller exiting on any error. The argv array is now heap-allocated to the exact size needed.
Ian Tay
·
2026-03-08
fix: only clean up socket on ConnectionRefused, not Timeout A 1-second probe timeout doesn't mean the daemon is dead — it may just be busy (heavy output, terminal resize reflow, serializing state for another client). Deleting a live daemon's socket orphans it permanently with no way to reach it via zmx commands. Applied to all callsites: get_session_entries, ensureSession (worst case: spawns a replacement daemon leaving the old one orphaned), kill, detachAll, history. For kill, the user now gets a helpful message if the daemon is busy vs. actually dead. The `zmx list` status label for error entries now says `status=unreachable` (not `cleaning up`) on Timeout, so the display doesn't contradict what we actually did. `zmx wait` now treats is_error entries as done+failed: on Timeout the socket persists, so without this the session would sit at task_ended_at==0 forever, defeating both the completion check and the zero-match timeout.
Ian Tay
·
2026-03-08
fix: signal handling — ignore SIGPIPE, handle EINTR in poll - Ignore SIGPIPE once in main() (inherited across fork, covers all subcommands). Without this, writing to a closed socket delivers SIGPIPE (default disposition: terminate) before write() can return EPIPE. An abrupt client exit killed the daemon; a daemon that died between probe and send killed one-shot clients (run/kill/history). - Handle EINTR in the daemon's poll loop (clientLoop already does this). Without this, SIGTERM/SIGCHLD caused the daemon to exit with an error instead of checking the sigterm flag and shutting down gracefully. Note: SA_RESTART is intentionally NOT set on SIGTERM/SIGWINCH. On BSD/macOS (unlike Linux), poll() is restartable when SA_RESTART is set — an idle daemon would never wake from poll() to check the sigterm flag. The EINTR handling in the poll loop is the correct and sufficient fix.
Ian Tay
·
2026-03-08
fix: use platform-correct O_NONBLOCK for fcntl; fix PTY write and double-close The hardcoded value 0o4000 is Linux-specific; on macOS O_NONBLOCK is 0x4. posix.SOCK.NONBLOCK is for socket()/accept4(), not fcntl(F_SETFL) — it only worked on Linux by coincidence where both constants share the same value. On macOS, non-blocking mode was never actually set on the PTY, client socket, or stdin. Setting O_NONBLOCK correctly exposes two latent issues, also fixed here: - PTY writes in handleInput/handleRun did `_ = try posix.write()`, discarding short-write counts and propagating WouldBlock as an error that crashed the daemon. Added ptyWriteAll() that polls on WouldBlock and retries short writes — same blocking semantics macOS had implicitly. - ensureSession's errdefer + defer both closed server_sock_fd when daemonLoop errored, causing EBADF (posix.close treats this as unreachable → panic in safe builds). Restructured so spawnPty failure is handled by an explicit catch; the defer is the sole owner after. Additionally, O_NONBLOCK is set on the open file description (shared with the parent shell), so stdin's original flags must be restored on exit to avoid leaving the parent shell's stdin in non-blocking mode.
Ian Tay
·
2026-03-08
fix(wait): time out after 3 polls if no matching sessions are found Follow-up to the upstream wait fix: the `total > 0` guard prevented vacuous exit 0, but left a typo'd `zmx wait foo` polling forever — trading a wrong answer for no answer. After 3 iterations (~3s) with max_seen still at 0, exit 2 with "no matching sessions found". 3s is generous: `zmx run foo && zmx wait foo` is essentially sequential (run blocks until the socket exists and the Run IPC is acked), so a persistent zero is a typo, not slowness.
Iván Hernández Cazorla
·
2026-03-08
chore: add link to openSUSE Tumbleweed package (#79)
Ian
·
2026-03-08
Fix verified bugs (#83) * fix: validate session names to prevent path traversal Session names become filenames under socket_dir. Without validation, `zmx attach ../foo` would create a socket outside that directory, and (worse) stale-socket cleanup via unlinkat(dirfd, "../foo") would delete files outside it. The ZMX_SESSION_PREFIX env var was similarly unsanitized. Also fixes a minor inefficiency: seshPrefix() was called twice. * fix(history): don't panic when only flags are provided `zmx history --vt` panicked on the .? unwrap when no positional session name was given. Pass empty string instead so getSeshName handles it (returns error.SessionNameRequired if no ZMX_SESSION_PREFIX is set either). * fix(wait): don't report vacuous success when no sessions match total == done was true when both were 0, so `zmx wait foo` returned exit 0 immediately if no session named foo existed — either because of a typo, or because of a race where the preceding `zmx run foo` hadn't yet created its socket. Now wait keeps polling until at least one matching session is seen. Also tracks the high-water mark of matched sessions: if the count drops, a session disappeared (daemon crashed or was killed), so wait errors out instead of looping forever. * fix(run): reset task state when receiving a Run command The daemon's exit-marker scan is gated on task_exit_code == null, but that field was never reset after the first task completed. A second `zmx run` on the same session would have its ZMX_TASK_COMPLETED marker ignored, causing `zmx wait` to immediately return the *first* task's exit code. Also explicitly set is_task_mode so `zmx run` works against sessions that were originally created by `zmx attach` (non-task mode). * fix(run): use CR not LF to submit commands to an already-prompting shell When `zmx run` sends a command to an existing session, the shell is at the readline prompt with the PTY in raw mode. readline binds accept-line to CR (\r), not LF (\n), so a trailing \n just moves the cursor — the command sits typed but unexecuted. The first-ever `zmx run` on a fresh session happened to work because it's sent during shell startup, before readline takes over: the line discipline is still canonical and ICRNL translates \n. Any subsequent run silently did nothing, which was masked by the stale-task-state bug (wait returned the first run's exit code, looking like success). \r works in both modes: canonical mode translates it via ICRNL, and raw-mode readline accepts it directly.
Danil Agafonov
·
2026-03-06
docs: add session picker recipe using fzf (#75) Add a shell function that fuzzy-finds and attaches to zmx sessions, previews scrollback history, or creates new ones. Includes a Ctrl-N keybinding to force session creation when a fuzzy match is highlighted. Particularly useful for remote SSH workflows.
Eric Bower
·
2026-03-04
refactor: break up main.zig This is just some code cleanup to make it a little easier to navigate the codebase. I don't want to break everything up into separate files but I want the main business logic to live inside of main.zig.
4d398e8
(single-quote-escape)
External.Kai-Fronsdal
·
2026-03-04
fix(run): always single-quote args to avoid bash history expansion bug The previous shellQuote preferred double quotes, but \! does not suppress history expansion inside double quotes in interactive bash. Switch to always using single quotes (matching Python's shlex.quote), which are universally safe since nothing is special inside single quotes except ' itself. Add unit tests for shellNeedsQuoting and shellQuote. Fixes #72 Made-with: Cursor
Eric Bower
·
2026-03-03
chore(list): changed key names and made formatting more stable
Eric Bower
·
2026-03-03
fix(run): stdin regression with ZMX_TASK_COMPLETED We were not actually running the command sent into stdin properly. Closes: https://github.com/neurosnap/zmx/issues/71
Eric Bower
·
2026-03-03
fix(run): when no client is attached, send DA response query from daemon
Eric Bower
·
2026-03-03
fix(run): re-quote when using shell meta chars argv doesn't receive the quotes in the command line so we need to apply an algorithm to re-quote args when it uses special meta-characters. Closes: https://github.com/neurosnap/zmx/issues/72
Justin Su
·
2026-02-26
Update fish shell instructions to use user completions directory (#64)
Justin Su
·
2026-02-26
Simplify `brew tap` and `brew install` into a single command (#65)
Noelle Leigh
·
2026-02-22
docs: Fix typo in CHANGELOG "stale" → "stall" Signed-off-by: Noelle Leigh <noelle_leigh@fastmail.com>
L.B.R.
·
2026-02-20
docs: add zmosh to community tools (#58) Co-authored-by: L.B.R. <lbr@mmonad.com>
Eric Bower
·
2026-02-19
feat: wait command This command will wait for all tasks to be completed and return an "aggregate" exit code.
Eric Bower
·
2026-02-17
feat: env var `ZMX_SESSION_PREFIX` This change will allow users to set an environment variable that prefixes all session names with the value provided in the env var. This should help with some automation tasks relying on zmx since you can now "group" sessions together under a common prefix.
Eric Bower
·
2026-02-08
feat(run): track task completion with a marker and record exit code
Michael Sakaluk
·
2026-02-19
docs: add zsm to community tools section in README (#55)
Paul Smith
·
2026-01-24
feat: indicate current session in listing This change adds a visual indicator (a prefixed arrow) of the current session, if any, to session listings with `zmx [l]ist`. Example: ``` $ zmx l session_name=o.quux pid=38719 clients=0 started_in=/home/example → session_name=o.foo.bash pid=60775 clients=2 started_in=/home/example session_name=o.foo.quux pid=50707 clients=1 started_in=/home/example session_name=o.zmx pid=21531 clients=2 started_in=/home/example session_name=o.zmx.bash pid=17772 clients=1 started_in=/home/example $ zmx l --short o.quux → o.foo.bash o.foo.quux o.zmx o.zmx.bash ``` If there is no current session, the output is unchanged. Fixes #42.
Josh Bainbridge
·
2026-01-01
docs: add documentation for evaluating auto-complete After adding support for auto-completion, we could add instructions for users on how to integrate this into their shell configurations. Update the README with a new sections for shell auto-complete configuration.
Josh Bainbridge
·
2026-01-01
feat: add completions command to output shell completion scripts Adding auto-completion support for a limited number of commonly used shells (bash, zsh, fish) would allow for greater shell integration. This can be done by using completions command that is then evaluated in the shell config. Add a new completions module that holds completion scripts for zsh, bash and fish. Then include a new command called completions that takes a shell name and outputs the requested script. This can then be added to a shell config such as .zshrc like so: eval “$(zmx completions zsh)" For automatic shell integration.
Josh Bainbridge
·
2026-01-01
feat: add new short list option via flag Listing just the session names, without extra information, could be useful for external tools or processes that need to do operations based on the session names. Add a new --short option to the list subcommand to list only session names without extra information.
Eric Bower
·
2026-01-23
feat(list): show `started_in` directory and original `cmd` if provided
Eric Bower
·
2026-01-21
fix: properly handle killing shells and children Shell's typically ignore SIGTERM unless there's a trap installed, but they will respond to SIGHUP. This works fine for shells but when a user runs `zig attach editor nvim` or any other non-shell command they might not respond to SIGHUP the way we want. So we have implemented a multi-signal approach: - SIGHUP - Wait 500ms - SIGKILL Reference: https://github.com/shell-pool/shpool/blob/1b3b27f059f0008c0509b5170d84b86c752b1da3/libshpool/src/daemon/shell.rs#L79-L94 Reference: https://github.com/neurosnap/zmx/pull/47
Eric Bower
·
2026-01-20
fix: gracefully shutdown daemon process This change properly handles when the daemon process receives a SIGTERM. Also during the kill process we properly forward SIGTERM to all children processes of the daemon.
Eric Bower
·
2026-01-09
feat(history): add `--vt` and `--html` flag The `--vt` flag will send the raw ansi escape sequences to recreate the terminal session. The idea is that this could be useful for debugging purposes. The `--html` flag was added because, why not? It's there, might as well use it.
Eric Bower
·
2026-01-08
fix: spawn as login shell
The `-{shell}` convention signals to most shells that they should behave as if invoked as a login
shell, loading appropriate startup files (like .bash_profile or .zprofile).
Reference: https://github.com/neurosnap/zmx/issues/41
Josh Bainbridge
·
2025-12-30
docs: add instructions on updating the bash shell prompt (#35) There are missing instructions on updating the prompt for bash. Combine both the zsh and the bash instructions in a generic setup.
Eric Bower
·
2025-12-29
fix: prevent terminal state corruption on detach When zmx sessions are nested through SSH (host zmx -> ssh -> remote zmx), the detach restore sequence was corrupting the outer session's terminal state tracking in ghostty-vt. The issue: when the remote zmx client detached, it sent clear screen and home cursor sequences (\x1b[2J\x1b[H) which flowed through SSH to the host's PTY. This caused the host's ghostty-vt to update its cursor position, and since nothing followed to correct it, subsequent reattaches to the host session would restore with incorrect cursor positioning. The fix: remove clear screen and home cursor from the detach sequence. We still reset terminal modes (mouse tracking, bracketed paste, alt screen, etc.) to ensure the terminal is left in a sane state. Trade-offs: - On detach, users now see the session content instead of a cleared screen. This is arguably useful feedback showing what was happening. - Nested zmx through SSH may still have cursor positioning issues since the restore's CUP sequence also flows through to the outer session. This is accepted as an edge case we cannot fully solve without breaking normal operation. - The attach clear is kept because it provides a clean slate and any corruption it causes is immediately overwritten by the session restore. Design principle: be conservative about terminal escape sequences sent on detach since they persist and affect the outer terminal's state. The session restore handles reconstruction, so we don't need to "clean up" on the way out. References: https://github.com/neurosnap/zmx/issues/31
Eric Bower
·
2025-12-28
feat: `zmx [r]un` to send a command to a session without attaching to it
Eric Bower
·
2025-12-27
refactor: remove ctrl+b + d detach shortcut This feature was annoying to implement because we can't simply check for a single byte, we first have to detect if ctrl+b was pressed then check for d. In the end I don't like the complexity it introduces and it seems pretty low value since we should recommend users simply close their terminal window.
Eric Bower
·
2025-12-27
docs: dont advertise alternative shortcut yet I might remove this shortcut since i'm noticing issues.
Eric Bower
·
2025-12-27
feat: `zmx [hi]story` command which prints session scrollback as text
Eric Bower
·
2025-12-20
fix: timing issue with first attach and sending terminal state On first attach we don't want to send the terminal state but there appears to be a timing issue where sometimes we already have pty output before the Init handler can complete.
Eric Bower
·
2025-12-19
fix: correct cursor position when reattaching to session Serialize terminal state BEFORE resizing the terminal in handleInit. Previously, the resize was done first, which caused cursor reflow and moved the cursor position. The shell's SIGWINCH-triggered prompt redraw would then run after our snapshot was sent, leaving the cursor at the wrong position (end of line instead of after the prompt). By capturing the terminal state before resize, we preserve the correct cursor position from when the user detached.
Eric Bower
·
2025-12-19
refactor: input shortcut handling Another code clarity change that abstracts handling the shortcuts for detach
Eric Bower
·
2025-12-19
refactor: daemonLoop and terminal serialization Code cleanup to make the daemonLoop and ghostty interaction easier to understand
Eric Bower
·
2025-12-19
feat: ctrl+b + d to detach With support to add more commands
Eric Bower
·
2025-12-19
fix: reset terminal modes on detach Closes: https://github.com/neurosnap/zmx/issues/26
Tristan Partin
·
2025-12-18
Use XDG_RUNTIME_DIR for the socket directory if defined XDG_RUNTIME_DIR[0] is a file path for user-specific runtime files, like sockets. It will typically be defined on Linux. I can't say for sure on BSDs, and I know for certain it is not defined on macOS. Link: https://specifications.freedesktop.org/basedir/latest/ [0] Signed-off-by: Tristan Partin <tristan@partin.io>
Eric Bower
·
2025-12-18
revert: don't force pty resize This seems to be causing issues with multiple clients connected, instead we will require clients to force resize themselves for now
Eric Bower
·
2025-12-15
fix: send SIGWINCH to PTY on re-attach TIOCSWINSZ alone doesn't notify the shell when terminal size is set. Now we explicitly signal the foreground process group after setting the window size, fixing display issues on re-attach.
Eric Bower
·
2025-12-11
fix(getTerminalSize): return default size if cols and rows are 0 The issue is that `getTerminalSize` can return 0x0 even when ioctl(TIOCGWINSZ) succeeds. The ghostty terminal library panics when initialized with zero dimensions. The fix in `getTerminalSize` now validates that both rows and cols are positive before using them, otherwise falling back to the default 24x80. Closes: https://github.com/neurosnap/zmx/issues/25
Eric Bower
·
2025-12-10
fix: flake.nix only supports linux atm `ghostty` does not support packaging nix on mac which I think means we will not be able to support it: - https://github.com/ghostty-org/ghostty/blob/cf06417b7dfbd0daeb58a9143f9b6ee194cbce26/nix/package.nix#L49 - https://github.com/ghostty-org/ghostty#2824 When our flake tries to build on macOS, it pulls ghostty as a Zig dependency, and ghostty's build.zig calls into apple-sdk/build.zig which needs the Darwin SDK — but ghostty's Nix infrastructure hasn't set that up.
Eric Bower
·
2025-12-05
refactor!: postfix uid for socket dir
- first we look for TMPDIR env var or else we default to `/tmp`
- then we look for ZMX_DIR env var or else we create a `/tmp/zmx-{uid}` folder
Since this is a breaking change, you can kill all of your previous
sessions by using `ZMX_DIR=/tmp/zmx zmx kill {sesh}`
Closes: https://github.com/neurosnap/zmx/issues/15
BREAKING CHANGE: this moves sockets to a new location which means none
of your previous sessions will be available
Eric Bower
·
2025-12-06
fix(attach): cannot attach to session within session Closes: https://github.com/neurosnap/zmx/issues/20
Eric Bower
·
2025-12-05
fix: support kitty keyboard ctrl+\ with event flags References: https://github.com/neurosnap/zmx/issues/10
Eric Bower
·
2025-12-03
refactor: when daemon is unresponsive, remove it This commit does a better job removing a unix socket file when the daemon is unresponsive, likely indicating that the process has already been killed. We accomplish this by creating a probSession fn that will connect to the unix socket, send the Info event, and timeout after 1 second. After that point we remove the unix socket file.
Eric Bower
·
2025-12-03
fix: send SIGTERM to pty child process before shutting down The issue was that when `zmx kill` was called, the daemon would call `shutdown()` and exit the loop, but then get stuck at `waitpid(daemon.pid, 0)` waiting for the PTY child shell to exit. Since the shell was never killed, the daemon would hang forever, preventing the defer block from running that deletes the socket file. The fix sends SIGTERM to the PTY child process before shutting down, allowing the shell to exit gracefully so waitpid returns and the socket cleanup defer runs. References: https://github.com/neurosnap/zmx/issues/10
Eric Bower
·
2025-12-03
fix: print help text with unknown commands We should print the help text when possible and refrain from using the logger when the user provides invalid input since that gets sent to our log files. Closes: https://github.com/neurosnap/zmx/issues/11
Eric Bower
·
2025-12-03
fix: ghostty should not send color pallet on re-attach When `zmx` starts a session, `ghostty-vt.Terminal.init()` uses Ghostty's default palette, not foot's palette. On reattach, it dumps all 256 colors from ghostty's defaults, which may differ from foot's defaults. References: https://github.com/neurosnap/zmx/issues/7
Eric Bower
·
2025-12-03
feat: increase max scrollback Closes: https://github.com/neurosnap/zmx/issues/9
Eric Bower
·
2025-12-02
fix: dont send ghostty snapshot on first attach chore: clear main screen and set cursor to home on first attach
Eric Bower
·
2025-12-02
fix: dont use alt screen on attach The problem is that the client switches to the alternate screen buffer on attach, which doesn't have scrollback. When ghostty replays content, it goes into the alt screen where scrollback doesn't accumulate. The alternate screen buffer (\x1b[?1049h) never has scrollback - that's by design in terminals. To get scrollback working properly we now stay on main screen and clear on detach.
Eric Bower
·
2025-11-27
feat: render terminal snapshot to client on re-attach This change uses libghostty-vt to restore the previous state of the terminal when a client re-attaches to a session. How it works: - user creates session `zmx attach term` - user interacts with terminal stdin - stdin gets sent to pty via daemon - daemon sends pty output to client *and* ghostty-vt - ghostty-vt holds terminal state and scrollback - user disconnects - user re-attaches to session - ghostty-vt sends terminal snapshot to client stdout In 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.
Eric Bower
·
2025-11-26
fix: discard any unread input when restoring terminal settings on detach
Eric Bower
·
2025-11-26
fix: free socket_path and the clients array backing storage before the daemon process exits
Eric Bower
·
2025-11-26
fix: close the server socket and delete the socket file if spawnPty fails
Eric Bower
·
2025-11-26
fix: client socket fd ensure the socket is closed when the function exits
Eric Bower
·
2025-11-26
fix: ensure the fd is closed if initUnix, bind, or listen fails
Eric Bower
·
2025-11-26
fix: use relative path since we are already inside the socket_dir
Eric Bower
·
2025-11-26
fix: properly allocate NUL-terminated copies of each argument the child process will exec (replacing memory) or exit, so no cleanup needed.
Eric Bower
·
2025-11-25
fix: cpu spike from unhandled events in our daemon and client loops
Eric Bower
·
2025-11-23
daemon-per-session (#4) This is a ground-up rewrite of this tool. We have dramatically simplified how this tool works. Instead of having a single daemon that manages all sessions, we have a daemon-per-session. We employ a simple architecture: `fork() -> setsid() on child -> forkpty() -> attach on parent` This has a bunch of benefits, one of which is we can safely remove libxev and use simple `poll()` loops for daemon and clients.
Eric Bower
·
2025-10-15
fix: filter our client input queries from echoing to stdout
Eric Bower
·
2025-10-15
feat: in-band resize window events https://gist.github.com/rockorager/e695fb2924d36b2bcf1fff4a3704bd83
Eric Bower
·
2025-10-15
refactor: move render terminal snapshot to separate module
Eric Bower
·
2025-10-13
refactor: increase pty buffer sizes This improves performance when writing to ghostty-vt
Eric Bower
·
2025-10-13
refactor(daemon): we dont need to buffer pty read for partial utf8 ghostty already handles this for us.
Eric Bower
·
2025-10-13
fix(attach): when daemon shuts down don't print EOF msg in attach
Eric Bower
·
2025-10-13
fix: respond to Device Attribute queries to prevent fish shell warning Fish shell sends a Primary Device Attribute (DA1) query (ESC [ c) when it starts to detect terminal capabilities. Previously, zmx forwarded this query to the PTY but never sent a response back to the client, causing fish to wait 2 seconds and display a terminal compatibility warning. This commit intercepts DA queries in handlePtyInput() before they reach the PTY and responds directly to the client with VT220 capabilities: - Primary DA (ESC [ c): VT220 with color (22) and clipboard (52) - Secondary DA (ESC [ > c): Terminal version info The response format matches ghostty's deviceAttributes implementation. Fixes the "fish could not read response to Primary Device Attribute query" warning on first attach.
Eric Bower
·
2025-10-12
fix: vt.resize on attach fix: filter out characters ghostty cannot handle
Tim Culverhouse
·
2025-10-13
support macOS and FreeBSD PTY functions (#1) Replace Linux-specific PTY headers with platform-specific imports to enable cross-platform compatibility. On macOS, use util.h for openpty(). On FreeBSD, use libutil.h for openpty(). On Linux and other platforms, continue using pty.h. Remove unused utmp.h include that was never referenced in the code. This change allows the daemon to compile and run on macOS and FreeBSD in addition to Linux without requiring platform-specific preprocessor directives or build configurations. Amp-Thread-ID: https://ampcode.com/threads/T-15fd58fa-5673-40c5-87e1-3ea0057599ca Co-authored-by: Amp <amp@ampcode.com>