Small tmux session controller with recurring sends
Project description
tmuxctl
tmuxctl is a small tmux workflow helper for three things:
- finding the session you want
- jumping to it quickly
- sending recurring follow-ups to long-running agent or worker sessions
It installs two executables:
tmuxctlt
t is just the shorter alias for the same CLI.
Install
Primary install:
uv tool install tmuxctl
Then use either:
tmuxctl --help
t --help
Run without arguments to see the 10 most recent sessions plus shortcut hints:
tmuxctl
t
Core Workflow
1. Find the session you want
Show all sessions, sorted by recency, with numeric IDs:
t list
Short form:
t l
tl
Typical output:
IDX SESSION CREATED
1 codex 2026-04-03 15:56:59
2 backend-worker 2026-04-03 15:22:10
3 docs 2026-04-03 14:10:31
If you just want the recent view:
t
t r
t recent --limit 10
2. Jump into a session
Attach by name:
t codex
That is equivalent to:
t attach codex
Ask tmux to resize the window after attaching:
t git-llm-zoomcamp --resize-window
t 1 -r
Attach by recency index:
t 1
t 2
t 10
Those resolve to attach-recent N.
Attach to the newest session directly:
t attach-last
3. Create a session if it does not exist
Use a leading colon when you want create-or-attach behavior:
t :codex
That resolves to:
t create-or-attach codex
Rule of thumb:
t codexmeans attach onlyt :codexmeans create or attach
Use t - to derive the session name from the current directory and create-or-attach it:
cd ~/git/workshops
t -
That resolves to:
t create-or-attach git-workshops
Pass a command after t - to run it only when a new session is created:
t - cy
If you want another session for the same folder, add any suffix:
cd ~/git/workshops
t -asd
That resolves to:
t create-or-attach git-workshops-asd
Create without attaching
t create-detached brings a memory-capped session into existence and returns
immediately, without occupying your terminal. It is for tools that attach over
their own transport (e.g. tmux -CC control mode) and would otherwise build raw,
uncapped new-session commands:
t create-detached myproj -c ~/git/myproj
It is idempotent (a no-op if the session already exists), resolves the memory cap
the same way as the other verbs (--mem flag → project cgroups.toml /
pyproject [tool.tmuxctl] → default), and prints the session name on success.
Memory and swap limits
tmux has one server process that owns all sessions. If one pane starts a runaway build, test VM, emulator, or agent process and the machine runs out of RAM, the kernel can kill that shared tmux server. When that happens, every tmux session disappears, including unrelated work.
tmuxctl avoids that by starting each new session's login shell through
systemd-run --user --scope:
tmux server
└── robust.slice
├── tmuxctl-project-a.scope MemoryMax=12G MemorySwapMax=8G
└── tmuxctl-project-b.scope MemoryMax=24G MemorySwapMax=8G
The tmux server stays outside those per-session scopes. Commands launched from
a pane inherit the cgroup of that session's shell, so memory accounting covers
the whole process tree for that session. If one session exceeds its hard
MemoryMax, systemd/kernel OOM handling kills processes inside that scope
instead of letting pressure spill into the shared tmux server and unrelated
sessions.
By default, new sessions get MemoryMax=12G and MemorySwapMax=8G. The memory
cap protects the tmux server from runaway work; the swap allowance lets a
transient spike survive instead of going straight to an OOM kill.
Set per-user defaults in ~/.config/tmuxctl/cgroups.toml:
default_mem = "24G"
default_swap = "8G"
slice_max = "56G"
slice_swap_max = "16G"
Set per-project defaults in either cgroups.toml:
mem = "24G"
swap = "8G"
or pyproject.toml:
[tool.tmuxctl]
mem = "24G"
swap = "8G"
swap = "0" is still valid when you want hard no-swap scope behavior.
--mem and config defaults apply when a session is created:
t :my-session --mem 30G
t create-detached my-session --mem 30G
For an existing tmuxctl-created session, change the live systemd scope with
limit:
t limit my-session --mem 30G
t limit my-session --mem 30G --swap 8G
t limit :current --swap 12G
Under the hood, this updates the session's systemd scope:
systemctl --user set-property tmuxctl-my-session.scope MemoryMax=24G MemorySwapMax=8G
Live changes are not written back to config. Use ~/.config/tmuxctl/cgroups.toml
or project config when you want future sessions to start with those limits.
4. Send a one-off message
Send text directly:
t send codex --message "check status and continue"
Or send from a file:
t send rk-codex --message-file prompts/rk-codex-progress.txt
By default, send waits 200ms before pressing Enter. You can change that:
t send codex --message "status?" --enter-delay-ms 500
t send codex --message "status?" --no-enter
Automation Workflow
1. Add a recurring job
Inline message:
t jobs add codex --every 15m --message "check status and continue"
If you are already inside tmux, use :current to target the active session without typing its name:
t jobs add :current --every 15m --message \
"Check project status and continue. Help any blocked agents, review CI, and \
keep the pipeline moving. If nothing in the current batch needs attention, \
pick the next two ready issues per _docs/PROCESS.md and run the full workflow."
Shared prompt file:
t jobs add rk-codex --every 30m --message-file prompts/rk-codex-progress.txt
When a job uses --message-file, tmuxctl stores the file path and reads the file at send time. Updating the file updates future scheduled runs.
2. Run the scheduler
t jobs daemon
Recurring jobs only run while the daemon is running.
3. Inspect and edit jobs
t jobs
t jobs list
t jobs show 2
t jobs logs --limit 20
t jobs edit 2 --every 45m
t jobs edit 2 --message "check status and continue"
t jobs edit 2 --session :current
t jobs edit 3 --message-file prompts/rk-codex-progress.txt
Useful job controls:
t jobs pause 3
t jobs pause-current
t jobs resume 3
t jobs resume-current
t jobs remove 3
If a scheduled job fails 3 runs in a row, tmuxctl jobs daemon removes it automatically.
Session Cleanup
Kill a session by name:
t kill codex
Kill a session by the numeric ID shown in t list:
t kill 2
Skip confirmation:
t k 2 --yes
Rename a session and retarget any scheduled jobs bound to it:
t rename codex codex-main
t rename 2 archived-worker
Inspect a Session
describe shows what is actually running inside a session — the process in each
pane, its working directory, the cgroup the session lives in, and (for sessions
started by t with a memory cap) live RAM and CPU usage read straight from that
cgroup. Target it by name, by the numeric ID from t list, or :current:
t describe codex # by name
t describe 2 # by the numeric ID from `t list`
t describe :current # the session you are in
For a capped session it reads memory and CPU from the session's
tmuxctl-<name>.scope cgroup, so the numbers cover the whole process tree, not
just the shell:
Session: git-myproj (1 window(s), 1 pane(s))
WIN.PANE PID COMMAND DIRECTORY
0.0* 3564734 claude /home/you/git/myproj
Scope: tmuxctl-git-myproj.scope (active)
Cgroup: /user.slice/.../robust.slice/tmuxctl-git-myproj.scope
Memory: 3.4G / 12.0G (peak 5.2G, swap 0B)
CPU time: 7m25s
Tasks: 222
A session you did not start through t has no memory cap. describe says so
and prints the real cgroup it found (e.g. a plain session-NN.scope), so you can
tell at a glance which sessions are protected and which can still take the whole
tmux server down under memory pressure:
Scope: none — session is uncapped (not started by tmuxctl)
Cgroup: /user.slice/.../session-7.scope
No per-session RAM/CPU cap; the box-wide OOM-killer can
take the whole tmux server. Start a capped one with:
t :git-myproj --mem 24G
Shell Setup
Bash completion
Install completion:
t --install-completion
Preview the script:
t --show-completion bash
Completion works for:
- commands
- plain session names
:sessionshortcuts
Local checkout helper
If you are working from this repository and want its virtualenv binaries on your PATH, run:
./install.sh
That appends this repo's .venv/bin and alias tl='t l' to ~/.bashrc, skipping any line that is already present.
How Scheduling Works
Recurring jobs are stored in:
~/.config/tmuxctl/tmuxctl.db
The scheduler is database-driven:
jobs addcreates jobsjobs edit,jobs pause,jobs resume, andjobs removemodify jobsjobs daemonpolls for due jobs and runs them
If you want recurring jobs to survive logout or reboot, keep t jobs daemon running with something like:
systemd --userlaunchdcron @reboot
Running as a systemd user service (Linux)
Create ~/.config/systemd/user/tmuxctl.service:
[Unit]
Description=tmuxctl scheduler daemon
After=default.target
[Service]
Type=simple
ExecStart=%h/.local/bin/tmuxctl jobs daemon
Restart=on-failure
RestartSec=5
[Install]
WantedBy=default.target
Adjust ExecStart to wherever tmuxctl is installed (for a local editable checkout, point at .venv/bin/tmuxctl). Then enable and start it:
systemctl --user daemon-reload
systemctl --user enable --now tmuxctl.service
systemctl --user status tmuxctl.service
To keep the daemon running after you log out, enable lingering for your user (needs sudo, one-time):
sudo loginctl enable-linger "$USER"
Logs are available via journalctl --user -u tmuxctl -f.
Known Problems
An orphaned scope can block recreating a session of the same name
Normally t kill tears a session's memory-capped scope down (systemctl --user stop tmuxctl-<name>.scope), so the unit name is free for next time. Two things
have to go wrong together to defeat that:
- the tmux server dies uncleanly (a crash or machine-wide OOM), so the normal kill path — and its scope teardown — never runs, and
- a disowned background process (e.g. an
Xvfb, a dev server, anythingnohup/&-launched) is still running inside that session's scope.
The dead session's shell is gone, but the stray process keeps the
tmuxctl-<name>.scope cgroup alive. The next time you try to create a session
with the same derived name (e.g. t - from the same folder), tmuxctl asks
systemd-run for that unit name and it fails with "Unit
tmuxctl-.scope was already loaded". The new tmux pane's command dies on
launch, and instead of a clear error you usually see terminal escape codes
(a Device-Attributes reply such as ^[[?61;...c) leak onto your prompt as the
aborted tmux client exits.
Diagnose — look for a scope whose tmux session no longer exists:
systemctl --user list-units --type=scope --all 'tmuxctl-*'
systemctl --user status tmuxctl-<name>.scope # shows the stray process holding it open
Fix — stop the orphan scope (this also kills the stray process inside it), then create the session again:
systemctl --user stop tmuxctl-<name>.scope
This is rare (it needs an unclean crash and a backgrounded process), so it is documented rather than worked around. A future version may auto-recover by clearing a stale scope whose session is gone before creating a new one.
Alternatives
Install with pip:
pip install tmuxctl
Install directly from GitHub:
uv tool install git+https://github.com/alexeygrigorev/tmuxctl.git
Install from a local checkout in editable mode:
git clone https://github.com/alexeygrigorev/tmuxctl.git
cd tmuxctl
uv tool install -e .
If you use the local checkout install, also run:
./install.sh
Reinstall the local checkout after updates:
uv tool install -e . --force
For development:
uv sync --dev
uv run pytest
uv build
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file tmuxctl-0.3.1.tar.gz.
File metadata
- Download URL: tmuxctl-0.3.1.tar.gz
- Upload date:
- Size: 46.6 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
11c4c121e7b789cde0ee8bac3a6f16fc0e082f16e6d34dd54d58b7d9888d8966
|
|
| MD5 |
fef8794d6565c6141e4352b3dda6f341
|
|
| BLAKE2b-256 |
3c32a4c26a2e556261f9308c2fcfacbc1dd5d270a9bcc981d60d4a059689a533
|
File details
Details for the file tmuxctl-0.3.1-py3-none-any.whl.
File metadata
- Download URL: tmuxctl-0.3.1-py3-none-any.whl
- Upload date:
- Size: 31.7 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
66ce5357e101c4e0eac1f3cd60b8fb7bac3943ff90a214f746433861a5aabb0f
|
|
| MD5 |
e39420b8964e95e5ca039d9a8e9918cd
|
|
| BLAKE2b-256 |
a448c873c68062aacda1995fc5efc996345eaddce64aec0f0702df4135a29f54
|