tmuxd
tmux + ttyd as a Python library: terminals that outlive the connection, that a program can type into and a person can open in a browser.
Everyone has written ttyd tmux new -A -s work at some point. tmux keeps the
session alive, ttyd makes it visible in a browser. It works — but the result has
no handle on it. Who opened that session, in which directory, is it still alive?
Want to feed it a command from the outside? SSH in and type tmux send-keys.
tmuxd is that command turned into something you import.
from tmuxd import Tmuxd
t = Tmuxd(port=12345, token="changeme") # ttyd is up; tmux is not yet
s = t.session(id="id5", cwd="~/proj", cmd="claude")
s.send("run the tests", enter=True)
print(s.url) # http://127.0.0.1:12345/?arg=id5
Send that URL to anyone and their browser is in that terminal — watching, and able to take over the keyboard. A program hands out the work; a person watches it run.
Quick start
Needs tmux (≥ 3.0) and ttyd on the machine — see Requirements.
As a library — no server needed
pip install tmuxd # zero runtime dependencies
from tmuxd import Tmuxd
with Tmuxd(port=12345, token="changeme") as t:
s = t.session(id="deploy", cwd="/srv/app", cmd="./deploy.sh")
print("watch it here:", s.url)
# ttyd goes with your process; the deploy is still running
Your process holds the instance, so there is nothing else to run.
From the command line — needs a server
pip install "tmuxd[server]" # + fastapi + uvicorn
tmuxd start # both ports picked free, recorded in ~/.tmuxd/daemon.json
tmuxd new -s work -c ~/proj
tmuxd send -s work "npm test" --enter
tmuxd url -s work -o # open it in a browser
tmuxd ls
tmuxd stop # stops the server; sessions keep running
A CLI command lives for milliseconds and can hold neither ttyd nor session state, so it asks a server that can. That is why the CLI and the server install together — why.
What makes it different
It is designed by subtraction. What was removed says more than what is left.
- One session is one terminal. No windows, no panes — the multiplexing half of tmux is not used. Want more terminals? Open more sessions.
- Write only, no reading. No
capture, norun, no output stream, no recording, no event stream. Reading a terminal belongs to a person (open the URL — ttyd already does that better than any API could) or to ssh (clean stdout, a real exit code, binary safety). What stays is the one write action neither of them can do. - The facade is short-lived, the house is not. ttyd is a child of your
process; the tmux server is nobody's child.
kill -9your program and the sessions carry on, with their working directory and command remembered. - It never touches your own tmux. Only the binary is probed, and the pool
always opens on a dedicated
tmux -L tmuxd. Yourtmux lsis unchanged. - A person and a program type into the same terminal. Not a feature we built — tmux gives it away, which is why the whole design is arranged around it.
- No permission tiers. Everything is read-write. Holding the token means holding a shell on that machine, so a read-only switch here would be a boundary that is not really there. Lock upstairs, where identity exists.
Two ways in
| Library | CLI | |
|---|---|---|
| Holds the instance | your process | tmuxd serve |
| Needs a server | no | yes |
| Install | pip install tmuxd |
pip install "tmuxd[server]" |
| Ports | ttyd only | ttyd + control API |
| Exposing it | mount tmuxd.server.router() in the app you already run |
control API (random port) |
Two ports, two audiences. One is ttyd and it is for people — s.url goes
straight to a colleague. The other is the control API and it is for programs —
JSON in, JSON out, seven endpoints. Both are free ports picked at startup: 7681
is ttyd's own default, which makes it the likeliest port for your own ttyd to be
on, and claiming it would be picking a fight. They are recorded in
~/.tmuxd/daemon.json, which every tmuxd command reads, so you never type them. Driving another machine is ssh box tmuxd …,
not a port on the internet.
Requirements
| tmux | ≥ 3.0 | apt install tmux · brew install tmux · dnf install tmux |
| ttyd | ≥ 1.6 | bundled in the Linux wheels · macOS: brew install ttyd |
| Python | ≥ 3.9 | |
| OS | Linux, macOS |
On Linux, pip install is enough. The wheels carry an upstream ttyd build
for their architecture (x86_64, aarch64, armv7l — glibc and musl alike, since
upstream links statically). A ttyd already on PATH still wins: that one can be
fixed by apt upgrade and ours can only be fixed by a release of tmuxd.
On macOS you install ttyd yourself — brew install ttyd. Upstream has never
shipped a Darwin build (checked back to 1.7.3: ten musl ELFs and one win32.exe,
every time), and Homebrew's is dynamically linked against five of its own
packages, so re-shipping it would do badly what brew does well. macOS gets the
py3-none-any wheel, which installs everywhere and simply expects ttyd on PATH.
Windows is not supported — tmux has no Windows build, and tmuxd imports
fcntl.
If your machine is not ready — an architecture no wheel covers, no tmux, or
you want a newer ttyd than the one we vendored — there is an optional
tmuxd install. It fetches a checksum-verified ttyd
from upstream (falling back to the bundled one when the network is down) and
tells you the exact command for tmux on this machine, then records both paths in
~/.tmuxd.json so the library and the CLI find them next time. A ready machine
never runs it, and Tmuxd() does not check whether you have.
tmuxd never adopts the tmux you use yourself: it runs its own pool on a
dedicated socket, so tmux ls shows exactly what it showed before.
Development
pip install -e ".[dev]"
pytest # ~198 tests, ~50s
pytest tests/exact_targeting -v # a single scenario
Tests run against real tmux, real ttyd and a real uvicorn — this project's whole value lives at the seam with those programs, and mocking them would test nothing. Each test gets its own tmux socket, so running the suite never disturbs a tmux you have open. They are organised by scenario, not by module.
License
Apache-2.0 — see LICENSE.
tmuxd drives ttyd (MIT) and tmux (ISC) as external programs; it neither vendors nor modifies them.
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distributions
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 tmuxd-2.1.0.tar.gz.
File metadata
- Download URL: tmuxd-2.1.0.tar.gz
- Upload date:
- Size: 167.7 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via:
twine/7.0.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
9d43d7a4a6d607a5dd3f16ed5adfb228d35a9512a8cc4c2812938f326706195e
|
|
| MD5 |
99e506b0bb002c262d4c7d0e745d0d3b
|
|
| BLAKE2b-256 |
4a2c9415c7447753cbbd55ed032b2dccdeae0e9b162b5730af07b34f34902718
|
Provenance
The following attestation bundles were made for tmuxd-2.1.0.tar.gz:
Publisher:
release.yml on memory-co/tmuxd
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
tmuxd-2.1.0.tar.gz -
Subject digest:
9d43d7a4a6d607a5dd3f16ed5adfb228d35a9512a8cc4c2812938f326706195e - Sigstore transparency entry: 2387785357
- Sigstore integration time:
-
Permalink:
memory-co/tmuxd@e4cf71adce0176e170ccf8f1f1b37bc8fe64455f -
Branch / Tag:
refs/tags/v2.1.0 - Owner: https://github.com/memory-co
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@e4cf71adce0176e170ccf8f1f1b37bc8fe64455f -
Trigger Event:
push
-
Statement type:
File details
Details for the file tmuxd-2.1.0-py3-none-manylinux_2_17_x86_64.musllinux_1_2_x86_64.whl.
File metadata
- Download URL: tmuxd-2.1.0-py3-none-manylinux_2_17_x86_64.musllinux_1_2_x86_64.whl
- Upload date:
- Size: 802.7 kB
- Tags: Python 3, manylinux: glibc 2.17+ x86-64, musllinux: musl 1.2+ x86-64
- Uploaded using Trusted Publishing? Yes
- Uploaded via:
twine/7.0.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
deb86608882c847b77c6f9c386343140dc097031533465ece14af8042a7b6bcc
|
|
| MD5 |
90b1ef683bfa7001d60fc95cb0f77a57
|
|
| BLAKE2b-256 |
10d9e8d2e9e1bd72cdaea2e140e742a51876fc9deb8298f8f8e0781fcc538d24
|
Provenance
The following attestation bundles were made for tmuxd-2.1.0-py3-none-manylinux_2_17_x86_64.musllinux_1_2_x86_64.whl:
Publisher:
release.yml on memory-co/tmuxd
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
tmuxd-2.1.0-py3-none-manylinux_2_17_x86_64.musllinux_1_2_x86_64.whl -
Subject digest:
deb86608882c847b77c6f9c386343140dc097031533465ece14af8042a7b6bcc - Sigstore transparency entry: 2387785367
- Sigstore integration time:
-
Permalink:
memory-co/tmuxd@e4cf71adce0176e170ccf8f1f1b37bc8fe64455f -
Branch / Tag:
refs/tags/v2.1.0 - Owner: https://github.com/memory-co
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@e4cf71adce0176e170ccf8f1f1b37bc8fe64455f -
Trigger Event:
push
-
Statement type:
File details
Details for the file tmuxd-2.1.0-py3-none-manylinux_2_17_armv7l.musllinux_1_2_armv7l.whl.
File metadata
- Download URL: tmuxd-2.1.0-py3-none-manylinux_2_17_armv7l.musllinux_1_2_armv7l.whl
- Upload date:
- Size: 771.0 kB
- Tags: Python 3, manylinux: glibc 2.17+ ARMv7l, musllinux: musl 1.2+ ARMv7l
- Uploaded using Trusted Publishing? Yes
- Uploaded via:
twine/7.0.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
c2aaf25a2dc177015dbc551913ea65bdc7c05eaae6a4a30d27a43cf9d8e5efb2
|
|
| MD5 |
99c80ec1d12b538799d48f2e2bb0df38
|
|
| BLAKE2b-256 |
b5bef8f93316f37628607be96f939bd09ba6d458205bf00476c079e6f9e3f8cf
|
Provenance
The following attestation bundles were made for tmuxd-2.1.0-py3-none-manylinux_2_17_armv7l.musllinux_1_2_armv7l.whl:
Publisher:
release.yml on memory-co/tmuxd
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
tmuxd-2.1.0-py3-none-manylinux_2_17_armv7l.musllinux_1_2_armv7l.whl -
Subject digest:
c2aaf25a2dc177015dbc551913ea65bdc7c05eaae6a4a30d27a43cf9d8e5efb2 - Sigstore transparency entry: 2387785388
- Sigstore integration time:
-
Permalink:
memory-co/tmuxd@e4cf71adce0176e170ccf8f1f1b37bc8fe64455f -
Branch / Tag:
refs/tags/v2.1.0 - Owner: https://github.com/memory-co
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@e4cf71adce0176e170ccf8f1f1b37bc8fe64455f -
Trigger Event:
push
-
Statement type:
File details
Details for the file tmuxd-2.1.0-py3-none-manylinux_2_17_aarch64.musllinux_1_2_aarch64.whl.
File metadata
- Download URL: tmuxd-2.1.0-py3-none-manylinux_2_17_aarch64.musllinux_1_2_aarch64.whl
- Upload date:
- Size: 808.4 kB
- Tags: Python 3, manylinux: glibc 2.17+ ARM64, musllinux: musl 1.2+ ARM64
- Uploaded using Trusted Publishing? Yes
- Uploaded via:
twine/7.0.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
2d4267270e1ce9fd278d610ce853a1f2a8d428e83fae650f3d7da2e53ef60212
|
|
| MD5 |
bb28446741df5c00a2f7b2b0d627324c
|
|
| BLAKE2b-256 |
d78dc95f5257e26bc2ce709f95e8a9ccfe491d1cb2d9e6d9f619b08cd0458e0f
|
Provenance
The following attestation bundles were made for tmuxd-2.1.0-py3-none-manylinux_2_17_aarch64.musllinux_1_2_aarch64.whl:
Publisher:
release.yml on memory-co/tmuxd
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
tmuxd-2.1.0-py3-none-manylinux_2_17_aarch64.musllinux_1_2_aarch64.whl -
Subject digest:
2d4267270e1ce9fd278d610ce853a1f2a8d428e83fae650f3d7da2e53ef60212 - Sigstore transparency entry: 2387785395
- Sigstore integration time:
-
Permalink:
memory-co/tmuxd@e4cf71adce0176e170ccf8f1f1b37bc8fe64455f -
Branch / Tag:
refs/tags/v2.1.0 - Owner: https://github.com/memory-co
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@e4cf71adce0176e170ccf8f1f1b37bc8fe64455f -
Trigger Event:
push
-
Statement type:
File details
Details for the file tmuxd-2.1.0-py3-none-any.whl.
File metadata
- Download URL: tmuxd-2.1.0-py3-none-any.whl
- Upload date:
- Size: 47.0 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via:
twine/7.0.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
f812e19f34a75677efce16363e519362ea8877ed3f868f26ee9d815e89ee2303
|
|
| MD5 |
b6ef6cac9ac6b48da1c36eaf2a782d57
|
|
| BLAKE2b-256 |
7b5ec9ea326ad01c6a4d8df7b54d0f8a52883bc0e4734270c12f41b32d52b967
|
Provenance
The following attestation bundles were made for tmuxd-2.1.0-py3-none-any.whl:
Publisher:
release.yml on memory-co/tmuxd
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
tmuxd-2.1.0-py3-none-any.whl -
Subject digest:
f812e19f34a75677efce16363e519362ea8877ed3f868f26ee9d815e89ee2303 - Sigstore transparency entry: 2387785374
- Sigstore integration time:
-
Permalink:
memory-co/tmuxd@e4cf71adce0176e170ccf8f1f1b37bc8fe64455f -
Branch / Tag:
refs/tags/v2.1.0 - Owner: https://github.com/memory-co
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@e4cf71adce0176e170ccf8f1f1b37bc8fe64455f -
Trigger Event:
push
-
Statement type: