Session attach/detach for the terminal
See the code
zmx
Session attach/detach for the terminal.
Docs
·
You might not need tmux
·
Sponsored by pico.sh
brew install neurosnap/tap/zmx
mise use zmx
Run immediately without installation:
nix run github:NixOS/nixpkgs/nixpkgs-unstable#zmx
# or build main yourself
nix run github:neurosnap/zmx
Start a shell with zmx available while it runs:
nix shell github:NixOS/nixpkgs/nixpkgs-unstable#zmx
# or built main yourself
nix run github:neurosnap/zmx
v0.16Be sure to add ~/.local/bin to your PATH:
zig build -Doptimize=ReleaseSafe --prefix ~/.local
[!IMPORTANT] We recommend closing the terminal window to detach from the session but you can also press
ctrl+\or runzmx detach. If you needctrl+\for something else (e.g. vim'sctrl+\ ctrl+nto escape its own:terminal), setZMX_NO_DETACH_KEYto disable the shortcut and rely onzmx detachor closing the window instead.
Run zmx help for more information on usage, with examples.
Usage: zmx <command> [args...]
Commands:
[a]ttach <name> [command...] Attach to session, creating if needed
[r]un <name> [-d] [command...] Send command without attaching
[s]end <name> <text...> Send raw input to session PTY
[p]rint <name> <text...> Inject text into session display
[wr]ite <name> <file_path> Write stdin to file_path through the session
[d]etach Detach all clients (ctrl+\\ for current client)
[l]ist|ls [--short] List active sessions
[g]et <name> Get session labels
set <name> k=v ... Set session labels
[cl]ear <name> Clear all session labels
print-env [-s] <name> [key] Print tracked environment variables
[k]ill <name>... [--force] Kill session and all attached clients
[hi]story <name> [--vt|--html] Output session scrollback
[w]ait <name>... Wait for session tasks to complete
[t]ail <name>... Follow session output
[c]ompletions <shell> Shell completions (bash, zsh, fish, nu, yash)
[v]ersion Show version and metadata (socket dir, log dir)
[h]elp Show this help
Nested sessions are not supported. Inside a session ZMX_SESSION is set, and attach reads it: instead of creating another client it switches the calling terminal to the session you named.
That matters when the variable is inherited rather than chosen. A script, build tool, or coding agent started inside a session runs with ZMX_SESSION set, so zmx attach other from there moves the terminal somebody was using, and the session it was showing is left with no client.
Unset it in anything that attaches on its own behalf:
env -u ZMX_SESSION zmx attach other
For non-interactive work prefer zmx run, which never switches the caller.
When 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.
We recommend checking for that env var inside your prompt and displaying some indication there.
Place this file in ~/.config/fish/config.fish:
functions -c fish_prompt _original_fish_prompt 2>/dev/null
function fish_prompt --description 'Write out the prompt'
if set -q ZMX_SESSION
echo -n "[$ZMX_SESSION] "
end
_original_fish_prompt
end
Depending on the shell, place this in either .bashrc or .zshrc:
if [[ -n $ZMX_SESSION ]]; then
export PS1="[$ZMX_SESSION] ${PS1}"
fi
powerlevel10k is a theme for zsh that overwrites the default prompt statusline.
Place this in .zshrc:
function prompt_my_zmx_session() {
if [[ -n $ZMX_SESSION ]]; then
p10k segment -b '%k' -f '%f' -t "[$ZMX_SESSION]"
fi
}
POWERLEVEL9K_RIGHT_PROMPT_ELEMENTS+=my_zmx_session
oh-my-posh 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:
[[blocks.segments]]
template = '{{ if .Env.ZMX_SESSION }} {{ .Env.ZMX_SESSION }}{{ end }}'
foreground = 'p:orange'
background = 'p:black'
type = 'text'
style = 'plain'
Starship 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:
format = """
${env_var.ZMX_SESSION}\
...
"""
[env_var.ZMX_SESSION]
symbol = " "
format = "[$symbol$env_value]($style) "
description = "zmx session name"
style = "bold magenta"
A session's environment is initialized when the session is created. When re-attaching from a new terminal or a different SSH connection, environment variables describing the client connection (such as SSH_AUTH_SOCK, DISPLAY, or terminal remote control variables like KITTY_LISTEN_ON, KITTY_PID, KITTY_WINDOW_ID) become outdated in the running shell.
zmx tracks these environment variables from attaching clients. You can inspect the current leader client's environment with zmx print-env:
# Print all tracked environment variables (set variables as KEY=VALUE, unset as -KEY)
zmx print-env .
zmx print-env dev
# Print POSIX shell commands (export ... / unset ...) suitable for eval
zmx print-env -s .
# Print a specific variable value
zmx print-env . SSH_AUTH_SOCK
You can automatically update your shell environment before each prompt:
Add to ~/.bashrc:
_zmx_env_hook() {
if [[ -n $ZMX_SESSION ]]; then
eval "$(zmx print-env -s .)"
fi
}
PROMPT_COMMAND="_zmx_env_hook${PROMPT_COMMAND:+; $PROMPT_COMMAND}"
Add to ~/.zshrc:
precmd() {
if [[ -n $ZMX_SESSION ]]; then
eval "$(zmx print-env -s .)"
fi
}
Add to ~/.config/fish/config.fish:
function _zmx_env_hook --on-event fish_prompt
if test -n "$ZMX_SESSION"
for line in (zmx print-env .)
if string match -q -- "-*" $line
set -e (string sub -s 2 $line)
else
set -gx (string split -m 1 "=" $line)
end
end
end
end
By default, zmx tracks:
DISPLAYSSH_AUTH_SOCKSSH_AGENT_PIDSSH_CONNECTIONWINDOWIDXAUTHORITYKITTY_LISTEN_ONKITTY_PIDKITTY_WINDOW_IDYou can customize this list by setting ZMX_TRACK_ENV to a comma-separated list of variable names:
export ZMX_TRACK_ENV="DISPLAY,SSH_AUTH_SOCK,GPG_AGENT_INFO"
Shell 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:
ssh remote-server zmx attach session-na<TAB>
# <- auto-complete suggestions appear here
NOTICE: when installing
zmxwithhomebrewcompletions are automatically installed.
Add this to your .bashrc file:
if command -v zmx &> /dev/null; then
eval "$(zmx completions bash)"
fi
Add this to your .zshrc file:
if command -v zmx &> /dev/null; then
eval "$(zmx completions zsh)"
fi
Add this to ~/.config/fish/completions/zmx.fish:
if type -q zmx
zmx completions fish | source
end
Add this to your ~/.config/yash/rc:
if command -v zmx > /dev/null 2>&1; then
eval "$(zmx completions yash)"
fi
Or save as an autoloaded script in your completion directory:
zmx completions yash > ~/.config/yash/completion/zmx
Add this to your Nushell config (config.nu):
use ("~/.cache/zmx/completions.nu" | path expand)
# Or generate dynamically:
zmx completions nu | save -f ~/.cache/zmx/completions.nu
You 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.
Requires fzf.
zmx-select() {
local display
local prefix="${ZMX_SESSION_PREFIX:-}"
display=$(zmx list 2>/dev/null | while IFS=$'\t' read -r name pid clients created dir; do
name=${name#*name=}
pid=${pid#*pid=}
clients=${clients#*clients=}
dir=${dir#*start_dir=}
if [[ -n "$prefix" ]]; then
[[ "$name" == "$prefix"* ]] || continue
name=${name#"$prefix"}
fi
printf "%-20s pid:%-8s clients:%-2s %s\n" "$name" "$pid" "$clients" "$dir"
done)
local output query key selected session_name
output=$({ [[ -n "$display" ]] && echo "$display"; } | fzf \
--print-query \
--expect=ctrl-n \
--height=80% \
--reverse \
--prompt="zmx> " \
--header="Enter: select | Ctrl-N: create new" \
--preview='zmx history {1}' \
--preview-window=right:60%:follow \
)
local rc=$?
query=$(echo "$output" | sed -n '1p')
key=$(echo "$output" | sed -n '2p')
selected=$(echo "$output" | sed -n '3p')
if [[ "$key" == "ctrl-n" && -n "$query" ]]; then
session_name="$query"
elif [[ $rc -eq 0 && -n "$selected" ]]; then
session_name=$(echo "$selected" | awk '{print $1}')
elif [[ -n "$query" ]]; then
session_name="$query"
else
return 130
fi
zmx attach "$session_name"
}
You 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.
if command -v zmx &> /dev/null && command -v fzf &> /dev/null && [[ -z "$ZMX_SESSION" ]]; then
zmx-select && exit
fi
If you use zmx on a shared server and SSH in frequently for quick operations, auto-launching the picker on every connection may be too aggressive. Instead, show a one-line reminder when active sessions exist:
if command -v zmx &> /dev/null && [[ -z "$ZMX_SESSION" ]]; then
local count
count=$(zmx ls --short 2>/dev/null | wc -l)
if [[ "$count" -gt 0 ]]; then
echo "zmx: $count session(s) active — \`zmx-select\` to attach" >&2
fi
fi
Choose the auto-launch pattern for dedicated dev machines, and the hint pattern for shared servers where you frequently run quick commands.
We 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.
export ZMX_SESSION_PREFIX="d."
zmx a runner # ZMX_SESSION=d.runner
zmx a tests # ZMX_SESSION=d.tests
zmx k tests # kills d.tests
zmx wait # suspends until all tasks prefixed with "d." are complete
The 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.
Instead, this tool specifically focuses on session persistence and defers window management to your os wm.
If you'd like to try out zmx and ssh without fiddling your ssh config, make sure to pass the -t option to ssh. Here's an example:
ssh -t dev-box zmx attach default
Without -t, the remote shell will not know it's talking to a terminal, and the display will likely get messed up.
This option isn't needed if you follow the configuration steps below, because RequestTTY yes does the same thing.
Using 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.
First, create an ssh config entry for your remote dev server:
Host = d.*
HostName 192.168.1.xxx
RemoteCommand zmx attach %k
RequestTTY yes
ControlPath ~/.ssh/cm-%r@%h:%p
ControlMaster auto
ControlPersist 10m
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!
Now you can spawn as many terminal sessions as you'd like:
ssh d.term
ssh d.irc
ssh d.pico
ssh d.dotfiles
Because the attach command is essentially an "upsert", this will create or attach to each session.
Now you can use the 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:
autossh -M 0 -q d.term
Or create an alias/abbr:
abbr -a ash "autossh -M 0 -q"
ash d.term
ash d.irc
ash d.pico
ash d.dotifles
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.
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.
Each session gets its own unix socket file. The default location depends on your environment variables (checked in priority order):
ZMX_DIR => uses exact path (e.g., /custom/path)XDG_RUNTIME_DIR => uses {XDG_RUNTIME_DIR}/zmx (recommended on Linux, typically results in /run/user/{uid}/zmx)TMPDIR => uses {TMPDIR}/zmx-{uid} (appends uid for multi-user safety)/tmp => uses /tmp/zmx-{uid} (default fallback, appends uid for multi-user safety)You can configure the permissions for the socket directory and log files using the following environment variables:
ZMX_DIR_MODE => sets the mode for the socket and log directories (octal, defaults to 0750)ZMX_LOG_MODE => sets the mode for the log files (octal, defaults to 0640)This 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.
We store global logs for cli commands in {log_dir}/zmx.log. We store session-specific logs in {log_dir}/{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.
The log directory is resolved in this order:
ZMX_DIR/logs if ZMX_DIR is setXDG_STATE_HOME/zmx/logs if XDG_STATE_HOME is setHOME/.local/state/zmx/logsTMPDIR/zmx-$UID (or /tmp/zmx-$UID) as a last resortzmx where we make changes to the underlying IPC communication, it will kill all your sessions because it cannot communicate through the daemon socket properlyzmx sessions through SSH: host A zmx -> SSH -> host B zmx
psql) so when you type it echos kitty escape sequences erroneouslydaemon and client processes communicate via a unix socketdaemon and client loops leverage poll(2)libghostty-vtWe use libghostty-vt to restore the previous state of the terminal when a client re-attaches to a session.
How it works:
zmx attach termghostty-vtghostty-vt holds terminal state and scrollbackghostty-vt sends terminal snapshot to client stdoutIn 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.
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.
https://github.com/shell-pool/shpool
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.
https://github.com/martanne/abduco
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.
| Feature | zmx | shpool | abduco | dtach | tmux |
|---|---|---|---|---|---|
| 1:1 Terminal emulator features | ✓ | ✓ | ✓ | ✓ | ✗ |
| Terminal state restore | ✓ | ✓ | ✗ | ✗ | ✓ |
| Window management | ✗ | ✗ | ✗ | ✗ | ✓ |
| Multiple clients per session | ✓ | ✗ | ✓ | ✓ | ✓ |
| Native scrollback | ✓ | ✓ | ✓ | ✓ | ✗ |
| Configurable detach key | ✗ | ✓ | ✓ | ✓ | ✓ |
| Auto-daemonize | ✓ | ✓ | ✓ | ✓ | ✓ |
| Daemon per session | ✓ | ✗ | ✓ | ✓ | ✗ |
| Session listing | ✓ | ✓ | ✓ | ✗ | ✓ |
(top 30 of 64)
Zig
90.8%
Shell
8.0%
Session attach/detach for the terminal
See the code
zmx
Session attach/detach for the terminal.
Docs
·
You might not need tmux
·
Sponsored by pico.sh
brew install neurosnap/tap/zmx
mise use zmx
Run immediately without installation:
nix run github:NixOS/nixpkgs/nixpkgs-unstable#zmx
# or build main yourself
nix run github:neurosnap/zmx
Start a shell with zmx available while it runs:
nix shell github:NixOS/nixpkgs/nixpkgs-unstable#zmx
# or built main yourself
nix run github:neurosnap/zmx
v0.16Be sure to add ~/.local/bin to your PATH:
zig build -Doptimize=ReleaseSafe --prefix ~/.local
[!IMPORTANT] We recommend closing the terminal window to detach from the session but you can also press
ctrl+\or runzmx detach. If you needctrl+\for something else (e.g. vim'sctrl+\ ctrl+nto escape its own:terminal), setZMX_NO_DETACH_KEYto disable the shortcut and rely onzmx detachor closing the window instead.
Run zmx help for more information on usage, with examples.
Usage: zmx <command> [args...]
Commands:
[a]ttach <name> [command...] Attach to session, creating if needed
[r]un <name> [-d] [command...] Send command without attaching
[s]end <name> <text...> Send raw input to session PTY
[p]rint <name> <text...> Inject text into session display
[wr]ite <name> <file_path> Write stdin to file_path through the session
[d]etach Detach all clients (ctrl+\\ for current client)
[l]ist|ls [--short] List active sessions
[g]et <name> Get session labels
set <name> k=v ... Set session labels
[cl]ear <name> Clear all session labels
print-env [-s] <name> [key] Print tracked environment variables
[k]ill <name>... [--force] Kill session and all attached clients
[hi]story <name> [--vt|--html] Output session scrollback
[w]ait <name>... Wait for session tasks to complete
[t]ail <name>... Follow session output
[c]ompletions <shell> Shell completions (bash, zsh, fish, nu, yash)
[v]ersion Show version and metadata (socket dir, log dir)
[h]elp Show this help
Nested sessions are not supported. Inside a session ZMX_SESSION is set, and attach reads it: instead of creating another client it switches the calling terminal to the session you named.
That matters when the variable is inherited rather than chosen. A script, build tool, or coding agent started inside a session runs with ZMX_SESSION set, so zmx attach other from there moves the terminal somebody was using, and the session it was showing is left with no client.
Unset it in anything that attaches on its own behalf:
env -u ZMX_SESSION zmx attach other
For non-interactive work prefer zmx run, which never switches the caller.
When 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.
We recommend checking for that env var inside your prompt and displaying some indication there.
Place this file in ~/.config/fish/config.fish:
functions -c fish_prompt _original_fish_prompt 2>/dev/null
function fish_prompt --description 'Write out the prompt'
if set -q ZMX_SESSION
echo -n "[$ZMX_SESSION] "
end
_original_fish_prompt
end
Depending on the shell, place this in either .bashrc or .zshrc:
if [[ -n $ZMX_SESSION ]]; then
export PS1="[$ZMX_SESSION] ${PS1}"
fi
powerlevel10k is a theme for zsh that overwrites the default prompt statusline.
Place this in .zshrc:
function prompt_my_zmx_session() {
if [[ -n $ZMX_SESSION ]]; then
p10k segment -b '%k' -f '%f' -t "[$ZMX_SESSION]"
fi
}
POWERLEVEL9K_RIGHT_PROMPT_ELEMENTS+=my_zmx_session
oh-my-posh 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:
[[blocks.segments]]
template = '{{ if .Env.ZMX_SESSION }} {{ .Env.ZMX_SESSION }}{{ end }}'
foreground = 'p:orange'
background = 'p:black'
type = 'text'
style = 'plain'
Starship 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:
format = """
${env_var.ZMX_SESSION}\
...
"""
[env_var.ZMX_SESSION]
symbol = " "
format = "[$symbol$env_value]($style) "
description = "zmx session name"
style = "bold magenta"
A session's environment is initialized when the session is created. When re-attaching from a new terminal or a different SSH connection, environment variables describing the client connection (such as SSH_AUTH_SOCK, DISPLAY, or terminal remote control variables like KITTY_LISTEN_ON, KITTY_PID, KITTY_WINDOW_ID) become outdated in the running shell.
zmx tracks these environment variables from attaching clients. You can inspect the current leader client's environment with zmx print-env:
# Print all tracked environment variables (set variables as KEY=VALUE, unset as -KEY)
zmx print-env .
zmx print-env dev
# Print POSIX shell commands (export ... / unset ...) suitable for eval
zmx print-env -s .
# Print a specific variable value
zmx print-env . SSH_AUTH_SOCK
You can automatically update your shell environment before each prompt:
Add to ~/.bashrc:
_zmx_env_hook() {
if [[ -n $ZMX_SESSION ]]; then
eval "$(zmx print-env -s .)"
fi
}
PROMPT_COMMAND="_zmx_env_hook${PROMPT_COMMAND:+; $PROMPT_COMMAND}"
Add to ~/.zshrc:
precmd() {
if [[ -n $ZMX_SESSION ]]; then
eval "$(zmx print-env -s .)"
fi
}
Add to ~/.config/fish/config.fish:
function _zmx_env_hook --on-event fish_prompt
if test -n "$ZMX_SESSION"
for line in (zmx print-env .)
if string match -q -- "-*" $line
set -e (string sub -s 2 $line)
else
set -gx (string split -m 1 "=" $line)
end
end
end
end
By default, zmx tracks:
DISPLAYSSH_AUTH_SOCKSSH_AGENT_PIDSSH_CONNECTIONWINDOWIDXAUTHORITYKITTY_LISTEN_ONKITTY_PIDKITTY_WINDOW_IDYou can customize this list by setting ZMX_TRACK_ENV to a comma-separated list of variable names:
export ZMX_TRACK_ENV="DISPLAY,SSH_AUTH_SOCK,GPG_AGENT_INFO"
Shell 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:
ssh remote-server zmx attach session-na<TAB>
# <- auto-complete suggestions appear here
NOTICE: when installing
zmxwithhomebrewcompletions are automatically installed.
Add this to your .bashrc file:
if command -v zmx &> /dev/null; then
eval "$(zmx completions bash)"
fi
Add this to your .zshrc file:
if command -v zmx &> /dev/null; then
eval "$(zmx completions zsh)"
fi
Add this to ~/.config/fish/completions/zmx.fish:
if type -q zmx
zmx completions fish | source
end
Add this to your ~/.config/yash/rc:
if command -v zmx > /dev/null 2>&1; then
eval "$(zmx completions yash)"
fi
Or save as an autoloaded script in your completion directory:
zmx completions yash > ~/.config/yash/completion/zmx
Add this to your Nushell config (config.nu):
use ("~/.cache/zmx/completions.nu" | path expand)
# Or generate dynamically:
zmx completions nu | save -f ~/.cache/zmx/completions.nu
You 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.
Requires fzf.
zmx-select() {
local display
local prefix="${ZMX_SESSION_PREFIX:-}"
display=$(zmx list 2>/dev/null | while IFS=$'\t' read -r name pid clients created dir; do
name=${name#*name=}
pid=${pid#*pid=}
clients=${clients#*clients=}
dir=${dir#*start_dir=}
if [[ -n "$prefix" ]]; then
[[ "$name" == "$prefix"* ]] || continue
name=${name#"$prefix"}
fi
printf "%-20s pid:%-8s clients:%-2s %s\n" "$name" "$pid" "$clients" "$dir"
done)
local output query key selected session_name
output=$({ [[ -n "$display" ]] && echo "$display"; } | fzf \
--print-query \
--expect=ctrl-n \
--height=80% \
--reverse \
--prompt="zmx> " \
--header="Enter: select | Ctrl-N: create new" \
--preview='zmx history {1}' \
--preview-window=right:60%:follow \
)
local rc=$?
query=$(echo "$output" | sed -n '1p')
key=$(echo "$output" | sed -n '2p')
selected=$(echo "$output" | sed -n '3p')
if [[ "$key" == "ctrl-n" && -n "$query" ]]; then
session_name="$query"
elif [[ $rc -eq 0 && -n "$selected" ]]; then
session_name=$(echo "$selected" | awk '{print $1}')
elif [[ -n "$query" ]]; then
session_name="$query"
else
return 130
fi
zmx attach "$session_name"
}
You 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.
if command -v zmx &> /dev/null && command -v fzf &> /dev/null && [[ -z "$ZMX_SESSION" ]]; then
zmx-select && exit
fi
If you use zmx on a shared server and SSH in frequently for quick operations, auto-launching the picker on every connection may be too aggressive. Instead, show a one-line reminder when active sessions exist:
if command -v zmx &> /dev/null && [[ -z "$ZMX_SESSION" ]]; then
local count
count=$(zmx ls --short 2>/dev/null | wc -l)
if [[ "$count" -gt 0 ]]; then
echo "zmx: $count session(s) active — \`zmx-select\` to attach" >&2
fi
fi
Choose the auto-launch pattern for dedicated dev machines, and the hint pattern for shared servers where you frequently run quick commands.
We 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.
export ZMX_SESSION_PREFIX="d."
zmx a runner # ZMX_SESSION=d.runner
zmx a tests # ZMX_SESSION=d.tests
zmx k tests # kills d.tests
zmx wait # suspends until all tasks prefixed with "d." are complete
The 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.
Instead, this tool specifically focuses on session persistence and defers window management to your os wm.
If you'd like to try out zmx and ssh without fiddling your ssh config, make sure to pass the -t option to ssh. Here's an example:
ssh -t dev-box zmx attach default
Without -t, the remote shell will not know it's talking to a terminal, and the display will likely get messed up.
This option isn't needed if you follow the configuration steps below, because RequestTTY yes does the same thing.
Using 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.
First, create an ssh config entry for your remote dev server:
Host = d.*
HostName 192.168.1.xxx
RemoteCommand zmx attach %k
RequestTTY yes
ControlPath ~/.ssh/cm-%r@%h:%p
ControlMaster auto
ControlPersist 10m
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!
Now you can spawn as many terminal sessions as you'd like:
ssh d.term
ssh d.irc
ssh d.pico
ssh d.dotfiles
Because the attach command is essentially an "upsert", this will create or attach to each session.
Now you can use the 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:
autossh -M 0 -q d.term
Or create an alias/abbr:
abbr -a ash "autossh -M 0 -q"
ash d.term
ash d.irc
ash d.pico
ash d.dotifles
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.
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.
Each session gets its own unix socket file. The default location depends on your environment variables (checked in priority order):
ZMX_DIR => uses exact path (e.g., /custom/path)XDG_RUNTIME_DIR => uses {XDG_RUNTIME_DIR}/zmx (recommended on Linux, typically results in /run/user/{uid}/zmx)TMPDIR => uses {TMPDIR}/zmx-{uid} (appends uid for multi-user safety)/tmp => uses /tmp/zmx-{uid} (default fallback, appends uid for multi-user safety)You can configure the permissions for the socket directory and log files using the following environment variables:
ZMX_DIR_MODE => sets the mode for the socket and log directories (octal, defaults to 0750)ZMX_LOG_MODE => sets the mode for the log files (octal, defaults to 0640)This 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.
We store global logs for cli commands in {log_dir}/zmx.log. We store session-specific logs in {log_dir}/{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.
The log directory is resolved in this order:
ZMX_DIR/logs if ZMX_DIR is setXDG_STATE_HOME/zmx/logs if XDG_STATE_HOME is setHOME/.local/state/zmx/logsTMPDIR/zmx-$UID (or /tmp/zmx-$UID) as a last resortzmx where we make changes to the underlying IPC communication, it will kill all your sessions because it cannot communicate through the daemon socket properlyzmx sessions through SSH: host A zmx -> SSH -> host B zmx
psql) so when you type it echos kitty escape sequences erroneouslydaemon and client processes communicate via a unix socketdaemon and client loops leverage poll(2)libghostty-vtWe use libghostty-vt to restore the previous state of the terminal when a client re-attaches to a session.
How it works:
zmx attach termghostty-vtghostty-vt holds terminal state and scrollbackghostty-vt sends terminal snapshot to client stdoutIn 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.
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.
https://github.com/shell-pool/shpool
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.
https://github.com/martanne/abduco
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.
| Feature | zmx | shpool | abduco | dtach | tmux |
|---|---|---|---|---|---|
| 1:1 Terminal emulator features | ✓ | ✓ | ✓ | ✓ | ✗ |
| Terminal state restore | ✓ | ✓ | ✗ | ✗ | ✓ |
| Window management | ✗ | ✗ | ✗ | ✗ | ✓ |
| Multiple clients per session | ✓ | ✗ | ✓ | ✓ | ✓ |
| Native scrollback | ✓ | ✓ | ✓ | ✓ | ✗ |
| Configurable detach key | ✗ | ✓ | ✓ | ✓ | ✓ |
| Auto-daemonize | ✓ | ✓ | ✓ | ✓ | ✓ |
| Daemon per session | ✓ | ✗ | ✓ | ✓ | ✗ |
| Session listing | ✓ | ✓ | ✓ | ✗ | ✓ |
(top 30 of 64)
Zig
90.8%
Shell
8.0%