A CLI tool for creating and managing tmux sessions declaratively from YAML configuration files.
- Declarative YAML config — one window per host, full custom layouts, or anything between
- Cssh-style broadcast —
sync: truefor synchronized typing across panes; tiled layouts - Ad-hoc multi-host sessions —
mox new @clusterormox new host1 host2 host3without editing config - Cluster expansion —
@nameresolves to a configured session's hosts or a clustersshclustersfile entry (with nested-cluster expansion) - Import existing tmux sessions — capture a running session's structure into the config (
mox import) - Local quick sessions —
mox newwith no args opens a single-pane local session;-tmakes it self-destruct on detach - In-tmux mode —
mox new -w @clusteropens a window in your current session instead of a new session - Configurable connect — defaults to
ssh {{host}}; override per session/window;ssh_user:shortcut - Reusable named layouts — define once, reference from any window
- Shell completion — bash, zsh, fish; completes sessions, clusters, layouts, and running tmux sessions
- Strict validation — typos in config keys error with line numbers
- Honest defaults — single binary, no daemon, no state
git clone https://github.com/bthall/mox.git
cd mox
make install # installs binary to $GOPATH/bin + shell completion for $SHELLmake install also installs completion for your current shell (bash, zsh,
or fish). If you only want completion (e.g. after go install), run
make install-completion.
go install github.com/bthall/mox/cmd/mox@latest(go install requires that the module has a published tag — until the first
release, build from source.)
Once a release is cut, archives for linux/macOS × amd64/arm64 will be
attached to the GitHub Releases page along with checksums.txt.
mox init # scaffold a default config at ~/.config/mox/config.yml
mox -a example # build + attach to the "example" session
mox list # configured + running tmux sessions
mox kill example # destroy a running sessionCommon workflows by example. Every short flag has a long form (mox new --help for the full table).
mox -a dev # build it if it isn't running, then attach
mox -a dev --force # tear down the running one and rebuild-a also attaches to any running tmux session, even ones not in your
mox config — useful for hand-rolled sessions you started by hand.
mox new # plain shell in a new tmux session (auto-named tmp-<timestamp>)
mox new -n work # named "work"
mox new -n work -r ~/proj # named "work", starting in ~/proj
mox new -t # destroyed automatically when you detachmox new with hosts gives you a tiled grid, synchronized typing across
panes, and sudo -i auto-sent so you type the root password once.
mox new host1 host2 host3 # 3 hosts, broadcast typing, sudo on connect
mox new @rps-api # same, using a configured session's host list
mox new -u root @rps-api # ssh as root
mox new -S=false @rps-api # turn off broadcast typing for this one
mox new --sudo=false @rps-api # skip sudoIf you're already inside tmux and don't want a separate session:
mox new -w @rps-api # new window in the current sessionmox new --from rps-api host42 # use rps-api's settings, but just this one hostIf you built a session interactively and want to recreate it later:
mox import work # add 'work' to your mox config under the same name
mox import work -n my-work # rename on import
mox import work -p # preview the YAML on stdout without saving
mox import work -F # overwrite an existing config entryPer-pane commands aren't recoverable from tmux's running state (send-keys
is one-way) — the imported session is structure-only. Add commands:
entries afterward to make it fully reproducible.
If you already maintain ~/.clusterssh/clusters, you don't have to
duplicate it. mox reads that file directly:
mox new @rps-monitoring # expands the clusterssh entry by that name
mox new @pve # nested clusterssh tags work tooConfigured sessions in ~/.config/mox/config.yml take precedence when a
name exists in both places.
After make install, <TAB> works for every argument that takes a name:
mox <TAB> # subcommands
mox -a <TAB> # configured + running tmux sessions
mox new @<TAB> # cluster names (config + clusterssh)
mox new --arrange <TAB> # tmux layout names
mox kill <TAB> # running tmux sessionsThe default config path follows the XDG Base Directory spec:
$XDG_CONFIG_HOME/mox/config.ymlif set, otherwise~/.config/mox/config.yml
Override via the --config flag (which expands ~).
The file is created with mode 0600 (owner read/write only) since it can
list hostnames and shell commands.
One window, one pane per host:
sessions:
devenv:
hosts: [api, web, worker]
root: ~/projects/myapp
commands:
- echo "Welcome!"The default connect template is ssh {{host}}. Override per session or per
window with the connect: field, or use the ssh_user: shortcut to prefix
the default with USER@:
sessions:
prod:
connect: "ssh -p 2222 ops@{{host}}"
hosts: [api1, api2]
as-root:
ssh_user: root # same as connect: "ssh root@{{host}}"
hosts: [a, b, c]
mixed:
windows:
- name: shells
connect: "mosh {{host}}" # window-level override
hosts: [bastion]
- name: local
hosts: [localhost] # uses default sshFor multi-host administration where you want one keystroke to broadcast to every pane:
sessions:
mongo-cluster:
sync: true # synchronize-panes on
arrange: tiled # grid layout instead of vertical strips
hosts: [mongo-1, mongo-2, mongo-3]
commands:
- sudo -i # password typed once, applied to allarrange: accepts any of tmux's built-in layouts: tiled, even-horizontal,
even-vertical, main-horizontal, main-vertical. Both sync: and
arrange: work at the session and window level (window overrides session).
sessions:
staging:
root: ~/staging
windows:
- name: backend
hosts: [api1, api2, api3]
- name: frontend
hosts: [web1, web2]split: root marks the first pane (no actual split — it's the pane that
exists when the window is created). split: horizontal stacks the new pane
under the previous; split: vertical places it side-by-side. size: is
a percentage of the parent pane (1–99).
sessions:
monitoring:
windows:
- name: system
panes:
- split: root
commands: [htop]
- split: horizontal
size: 30
commands: [df -h]layouts:
two-pane:
panes:
- split: root
- split: vertical
size: 30
sessions:
dev:
windows:
- name: logs
layout: two-paneSee examples/config.yml for more.
Session lifecycle:
mox -a <session> attach to a configured session (builds it if not running)
also attaches to any running tmux session by name
mox new [hosts...] ad-hoc session or window (alias: cssh)
mox list | ls list configured and running sessions
mox kill <session> destroy a running session
mox import <session> capture a running tmux session into the config
Configuration:
mox init scaffold a default config
mox validate check config syntax
mox config path print resolved config path
mox config view print the raw config file
Other:
mox completion <sh> emit a shell-completion script
mox --version print the build version
mox <command> --help shows the full flag list for any command, including
shorthands and defaults.
Anywhere mox new accepts a host, @name expands to a host list.
Lookup order:
- The
hosts:of a mox configured session namedname(complex sessions are flattened across all their simple-mode windows) - The
nameentry in~/.clusterssh/clusters(or$CSSH_CLUSTERS, or/etc/clusters). Nested clusters are expanded recursively with cycle detection.
When name exists in both, the mox config wins. Mix freely with literal
hosts on the same line.
make install does this automatically for your $SHELL. To install it
standalone, or after go install:
make install-completion # detects $SHELL, installs to user dir
# or manually:
mox completion bash > ~/.local/share/bash-completion/completions/mox
mox completion zsh > ~/.zsh/completions/_mox
mox completion fish > ~/.config/fish/completions/mox.fish-c, --config <path>— override the config path (~expanded)-v, --verbose— debug logging to stderr-q, --quiet— only warnings and errors--force—mox -a <session> --forcerebuilds the session even if it's running
When -a attaches from inside an existing tmux client ($TMUX set),
mox uses switch-client instead of attach-session, so nested-session
errors are avoided automatically.
Ctrl-C during session creation cancels cleanly: the partial session is
killed before the program exits.
- Session and window names cannot contain
: . $or whitespace (these are special in tmux's target syntax) - Hostnames in the default
ssh {{host}}template must match^[A-Za-z0-9._%@-]+$. To use other characters, overrideconnect:with a template that handles your own escaping - Unknown YAML keys are an error (catches typos like
hots:forhosts:) - A session can define
hosts,windows, or neither (a session with neither opens a single local pane — useful forcommands:-only workflows) - Inside a window,
hostsis mutually exclusive withpanesandlayout, andpanesis mutually exclusive withlayout - Multiple windows in a session may share the same name (tmux addresses windows by id, not name)
The config file is treated as trusted user input — commands: and the
connect: template are executed in the spawned tmux pane via send-keys.
Sharing a config is equivalent to sharing arbitrary shell commands.
See SECURITY.md for vulnerability reporting.
make build # build to ./build/mox
make test # unit tests
make integration # tests requiring a live tmux server
make lint # golangci-lint
make vuln # govulncheck scanCI runs the unit tests + lint + govulncheck on every push, plus integration
tests on Linux. See CONTRIBUTING.md for details.
MIT
