A lightweight, dependency-free Python CLI that builds tmux sessions from TOML profiles. Describe your windows, panes, and commands once, then launch a full working environment with a single command.
- No dependencies — just Python 3 and tmux.
- Declarative — sessions are plain TOML, easy to version and share.
- Idempotent — re-running attaches to an existing session instead of duplicating it.
| Dependency | Version | Notes |
|---|---|---|
| Python | 3.11+ | Standard library only — no pip packages to install. Uses tomllib (added in 3.11) to read profiles. |
| tmux | any recent | The tmux binary must be on your PATH. |
The commands inside your profiles (e.g. nvim, git, lazygit) are yours to provide — tmx just launches whatever you put in them.
tmx is developed as small modules under src/ and bundled into a single self-contained executable with Python's standard-library zipapp — so there are no third-party build tools either. The installed tmx is one file you can copy anywhere a compatible Python lives.
git clone https://github.com/joaoluga/tmx-session.git
cd tmx-sessionmake build # bundle src/ into the single-file `tmx` executable
make deploy # build + install `tmx` to ~/.local/bin
make install-profiles # copy the example profile to ~/.config/tmux/profiles
make install-all # install + install-profilesOverride the locations if you like:
make install PREFIX=/usr/local # -> /usr/local/bin/tmx
make install-profiles PROFILE_DIR=~/my/profilesMake sure the chosen bin directory is on your PATH. make help lists every target, and make uninstall removes the script (your profiles are left untouched).
python3 -m zipapp src -p '/usr/bin/env python3' --compress -o tmx
install -Dm755 tmx ~/.local/bin/tmx
mkdir -p ~/.config/tmux/profiles
cp profiles/*.toml ~/.config/tmux/profiles/tmx dev # create + attach to the "dev" session
tmx dev music # launch multiple profiles, attach to the first
tmx -fr dev # force-reload: kill the session, then recreate it
tmx -l # list available profiles
tmx -k dev # kill a running session
tmx -s dev # save a running session as a new profile (profiles/dev.toml)
tmx -s dev -o - # ...or print it to stdout (e.g. to redirect into a repo)
tmx -u dev # sync session changes back into the existing dev profile
tmx -h # help-
tmx --savewrites to$TMX_PROFILE_DIR/<session>.tomlby default. If that directory is read-only (e.g. deployed by home-manager/nix), use-o -to print to stdout, or-o <path>to write a file or into another directory. -
tmx --syncupdates an existing profile to match the running session: windows present in both are updated in place, windows you've added are appended (it never deletes ones you've closed), androot/on_attachare kept. It defends your profile against tmux's blind spots — a pane whose program has exited, or whose command tmux reports without its arguments (sleepforsleep 999), keeps the command already in the profile. It prints a diff and keeps a.bak; comments are not preserved when something changes (tomllib can't round-trip them), but a no-op sync leaves the file — and its comments — untouched.-o/-o -work as with--save. -
If a session already exists,
tmxattaches to it instead of recreating. -
When run from inside tmux, it uses
switch-clientinstead ofattach. -
Launching several profiles creates them all up front and attaches to the first one; the rest keep running in the background.
Profiles are TOML files read from ~/.config/tmux/profiles/. Set the TMX_PROFILE_DIR environment variable to read them from somewhere else:
TMX_PROFILE_DIR=~/dotfiles/tmux/profiles tmx devA complete, runnable example ships in profiles/example.toml — copy it and adapt it to your own tools.
session = "name"
root = "~/default/dir" # optional (default: ~)
on_attach = "window-to-focus" # optional (default: first window)
[[windows]]
name = "editor"
dir = "~/override/dir" # optional (inherits root)
command = "nvim"
[[windows]]
name = "git"
split = "horizontal" # panes left/right, sized by percentage
[[windows.panes]] # first pane = the window itself
size = 70
[[windows.panes]]
command = "lazygit"
size = 30| Field | Required | Description |
|---|---|---|
session |
yes | Tmux session name |
root |
no | Default working directory (default: ~) |
on_attach |
no | Window to focus after creation (default: first window) |
windows |
yes | List of windows (at least one) |
windows[].name |
yes | Window name |
windows[].dir |
no | Working directory (inherits root) |
windows[].command |
no | Command to run (omit for a plain shell) |
windows[].panes |
no | Split into panes (takes precedence over command) |
windows[].panes[].command |
no | Pane command (omit for a plain shell) |
windows[].panes[].dir |
no | Pane directory (inherits the window's dir) |
windows[].panes[].size |
no | Pane's percent of the window along split (omit for an even share) |
windows[].split |
no | Size panes by percent: horizontal (left/right) or vertical (stacked) |
windows[].layout |
no | A named preset (even-horizontal, even-vertical, main-horizontal, main-vertical, tiled) or a raw tmux layout string. Alternative to split. |
Notes
- The first pane of a
paneswindow is the window itself; its directory is the window'sdir/root(adirset on the first pane is ignored). - If a window defines both
panesandcommand,paneswins. - Pane sizing. Use
split+ per-panesizefor percentages (tmxgenerates the exact tmux layout for those proportions), orlayoutfor a named preset / captured string. If both are set,splitwins. Percentages are approximate to the cell — tmux rounds to whole rows/columns.
Drop a new TOML file into your profile directory and run it:
cat > ~/.config/tmux/profiles/work.toml << 'EOF'
session = "work"
root = "~/code/work"
[[windows]]
name = "code"
command = "nvim"
[[windows]]
name = "shell"
EOF
tmx workIf you manage your dotfiles separately, keep the source profiles in your dotfiles repo and either symlink them into ~/.config/tmux/profiles/ or point TMX_PROFILE_DIR at them.
tmx-session/
├── src/ # source modules (Python 3, stdlib only)
│ ├── __main__.py # entry point
│ ├── cli.py # argument parsing + dispatch
│ ├── commands.py # the CLI verbs: load / save / sync / list / kill / launch
│ ├── introspect.py # live session -> Profile capture + sync merge
│ ├── model.py # Pane / Window / Profile + TOML (de)serialization
│ ├── layout.py # tmux layout-string math
│ ├── tmux.py # the `tmux` command wrapper
│ ├── tomlio.py # TOML read helpers + basic-string emitter
│ └── util.py # shared helpers + constants
├── tmx # the built single-file executable (zipapp; gitignored)
├── Makefile # build / install / uninstall targets
├── profiles/
│ └── example.toml # example profile
├── README.md
└── LICENSE
tmx is a zipapp bundle of src/ — a single executable file (a shebang + a zip of the modules), built by make and ignored by git. Edit the modules in src/; run make build (or python3 src to run straight from source without building).
The shipped tool is stdlib-only, but development uses a small quality gate. With uv installed (it fetches the tools on demand — nothing is added to what users install):
make lint # ruff lint
make typecheck # mypy + basedpyright (strict)
make check # lint + typecheck + build — the same gate CI runsGitHub Actions runs make lint / make typecheck and builds + smoke-tests the zipapp on Python 3.11–3.14 for every push and pull request.
tmx and tmux-resurrect / tmux-continuum solve different halves of the same problem, and pair naturally:
tmxis the declarative layout — what a session should look like from scratch: which windows, panes, working directories, and startup commands. It's version-controlled TOML you can share and reproduce on any machine.- resurrect / continuum is the runtime state — what was actually running last time: the live processes, pane contents, and cursor positions, which continuum autosaves on an interval and restores when the tmux server starts.
A typical workflow:
- Define your sessions once as profiles and launch them with
tmx dev. - Work normally — continuum keeps autosaving in the background.
- After a reboot, continuum restores the sessions automatically; running
tmx devagain just reattaches (it's idempotent), so the same command works whether the session is brand new or being resumed.
In short: tmx gives you a clean, repeatable starting point; resurrect/continuum preserve the state you build up on top of it.
Start a profile automatically from your compositor or login shell, e.g. Hyprland:
exec-once = env -u TMUX kitty ~/.local/bin/tmx dev