Skip to main content

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:

  • tmuxctl
  • t

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 codex means attach only
  • t :codex means 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.

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
  • :session shortcuts

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 add creates jobs
  • jobs edit, jobs pause, jobs resume, and jobs remove modify jobs
  • jobs daemon polls 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 --user
  • launchd
  • cron @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:

  1. the tmux server dies uncleanly (a crash or machine-wide OOM), so the normal kill path — and its scope teardown — never runs, and
  2. a disowned background process (e.g. an Xvfb, a dev server, anything nohup/&-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


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

tmuxctl-0.3.0.tar.gz (43.4 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

tmuxctl-0.3.0-py3-none-any.whl (30.1 kB view details)

Uploaded Python 3

File details

Details for the file tmuxctl-0.3.0.tar.gz.

File metadata

  • Download URL: tmuxctl-0.3.0.tar.gz
  • Upload date:
  • Size: 43.4 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.1.0 CPython/3.13.12

File hashes

Hashes for tmuxctl-0.3.0.tar.gz
Algorithm Hash digest
SHA256 e4a0973f686d273cfd4203c844b29a24f6c4eb0ee0f658aafcb6e0f074b0f238
MD5 35f11e3e7c671ce5423c33272ecf289e
BLAKE2b-256 2192e28e1b09c0d2d85d5a2092822d6f254cdc435582b8d62d27055a3a932b2f

See more details on using hashes here.

File details

Details for the file tmuxctl-0.3.0-py3-none-any.whl.

File metadata

  • Download URL: tmuxctl-0.3.0-py3-none-any.whl
  • Upload date:
  • Size: 30.1 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.1.0 CPython/3.13.12

File hashes

Hashes for tmuxctl-0.3.0-py3-none-any.whl
Algorithm Hash digest
SHA256 9111863ae2a0e2002b374f52d77919840b8dd02c78f4452faf9c2f82a5eb401b
MD5 7510a9de0becf9c1288c0c9664e4b340
BLAKE2b-256 4234e6388fc2cf09c4a3ed982213fa6aed69f5b6af62dc2785fe1d9dd79797cb

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page