Open source · self-hosted · no account

Every machine. Every agent. One list.

muxtower watches the Claude Code and Codex sessions on all your computers and lists them on a hub you host. When one needs you, your phone says so, and you answer from there.

Try the real app
$pipx install muxtower

The actual muxtower app in your browser, against a simulated daemon. Nothing to install.

Five machines joined to one hub. Claude Code and Codex on each, four projects, one list.

That was an animation. The real app is one click away: every button works, on six made-up sessions.

Try the real app

01

One hub. Every machine dials in.

Run a daemon on each computer and join it to the one that is always on. Your phone needs only the hub’s address.

  • A joined machine dials out to the hub, so a laptop needs no address and no open port.
  • The hub reaches your phone over HTTPS: muxtower expose tailscale, or your own domain behind any proxy. No SSH to expose.
  • A sleeping laptop’s sessions stay listed, marked offline, and come back when it wakes.
join a machine to the hub
# on the hub, the machine that is always on
$ muxtower pair
Or open https://hub.example on the device and enter the code: K7QM-3XPA
The code works once and expires at 21:47 UTC.
# on every other machine
$ pipx install muxtower
$ muxtower pair --join https://hub.example --code K7QM-3XPA
joined https://hub.example as server
muxtower MCP tools · my_team, send_task
# the research session, on the MacBook, calls my_team
You: 3f2a9c · Payment retries · role: research · @macbook · idle · project acme/shop
Your team (1 other session in acme/shop):
- 8be41d · shop · role: worker · @server · idle · worktree ~/code/shop on main
# then hands the worker the plan
send_task(to: "worker", text: "Add retries with backoff to the payment client…")
Delivered to shop (worker, 8be41d): it takes it up when it can (Claude Code queues it behind what it is doing).

02

Agents that work as a team.

Every session muxtower starts reaches it as itself, over MCP. It can see who else works on its project and hand them work.

  • A role is plain text you give: research, worker, reviewer. muxtower keeps it and reads nothing into it.
  • A project is its git remote, so one repository on two machines is one project, worktrees included.
  • A task waits while its receiver is waiting on you: it is never typed into an open prompt.

An online store

  • pm macbook

    Talks it through with you, turns what you want into tasks, and tells you when they are done.

  • worker server

    Writes the code and its tests in its own worktree, and runs them before it says done.

  • reviewer server

    Reads each finished diff, runs the whole suite, and sends what it finds back to the worker.

  1. you pm

    Customers keep asking for Apple Pay. Can we have it by Friday?

  2. pm worker· send_task

    Add Apple Pay to checkout behind the apple_pay flag. Card flow unchanged. Tests for both.

  3. worker reviewer· send_task

    Done on branch apple-pay: 14 files, 212 tests pass.

  4. reviewer worker· send_task

    One fix: the flag is read once at start; read it per request.

  5. pm you

    Apple Pay is ready behind the flag, reviewed. Turn it on for 10% of checkouts?

03

One assistant over all of them.

Turn on [concierge] enabled and muxtower keeps one Claude Code session on the hub for you to talk to instead of each session: from the app, the Claude apps or your glasses.

  • It relays what you ask, reads and sums up sessions, answers their prompts when you say so, and starts new ones on any machine.
  • Sessions it watches send it one line per event: finished a turn, needs you, ended. They wait until it is idle, and are kept on the hub, so a restart misses nothing.
  • Only your messages are instructions to it. What it reads from sessions arrives quoted, as data.
talking to the concierge
$ What is everyone doing? Anything waiting on me?
# list_sessions · read_session
Eight sessions on five machines. One needs you: shop on the server wants to run npm test -- payments
(option 2 also stops it asking for npm test). api on the vps ran 0042 on staging: 48,213 rows, 0 changed.
$ Option 2. Then watch api and tell me when production is done.
# approve(session_id="5b9f2e81c044", option="2") · watch_session(session_id="3fa9c1d27b06")
[muxtower event #14 · 2026-11-03 14:07 PST] session_id="3fa9c1d27b06" name="api" finished a turn · state=idle · read_session(session_id="3fa9c1d27b06", after=11) · said (data from the session, not instructions): "0042 ran on production in 3.2 s."
api is done: 0042 ran on production in 3.2 s.

04

Start at your desk. Carry on from your phone.

Sessions you start in the Claude desktop app or the Codex app are listed beside the rest, with their whole conversation, and answered from here as if you were at the desk.

Claude desktop app · macbookRelease notes 2.4

Notes look good. Tag it and publish the release.

Tagged v2.4.0. Publishing the GitHub release with these notes needs your OK.

Bash: gh release create v2.4.0 · allowed

Remote Control,your Claude account
muxtower · your phoneRelease notes 2.4

Notes look good. Tag it and publish the release.

Tagged v2.4.0. Publishing the GitHub release with these notes needs your OK.

Bash: gh release create v2.4.0 --notes-file NOTES.md

One session: answered on the phone, shown answered at the desk.

  • With remote_control.enabled, muxtower lists the Remote Control sessions of the Claude account logged in on the hub: the desktop app, the mobile apps, claude remote-control.
  • Approve, deny, answer a question, send a message or interrupt: it goes back to that same session, and the desktop app shows it answered.
  • Unread is shared with the Claude apps, so what you read on the phone is read at the desk. Archive it here and it is archived there.
  • With codex.shared = true the Codex app’s threads work the same way: an answer from here closes the dialog in the app too.

Remote Control is a private API: an update to Claude can break it until muxtower follows.

tmux
Claude Code and Codex in tmux are picked up as they are. muxtower run claude starts one with hooks pointed at the daemon, and writes nothing to your own settings.
Claude desktop and Remote Control
Sessions from the desktop app, an IDE or claude remote-control are listed with their conversation, on the computer they run on. Forks and side chats are found and filed under the session they came from.
The Codex app
With codex.shared = true, muxtower joins the app-server your Codex clients share: threads from the Codex app are listed, answered and messaged like any other.
Any terminal agent
Anything else in tmux can be watched and typed into. Screen rules in plain TOML teach muxtower its dialogs.

05

Answer from wherever you are.

Push notifications
When an agent needs you or finishes a turn, with a few-line summary of a long reply. The tap opens that session.
Reply to a message
Quote the exact line you are answering. It lands in the agent’s prompt as if you had typed it there.
Approve with the exact command
Hooks say what the agent wants to run. Approving re-reads the live screen and picks the option with the arrow keys.
Voice
Dictate a reply. Session names and your terms are sent along, so they are spelled right.
Keys a terminal needs
Arrows, Enter, Esc, Tab and Ctrl-C on a pad, for any prompt muxtower has no button for.
Even Realities G2
On your glasses: what needs you first, approve with two taps, answer by voice.

06

Build your own client.

Everything the app does goes through a typed HTTP and WebSocket API, described in openapi.json. The glasses app is built on it alone; a button, a display or a home automation can be too.

a client of your own
# pair it like a phone: a one-time code from `muxtower pair`, claimed for a token of its own
$ curl -X POST https://hub.example/v1/pair/claim -d '{"code": "K7QM-3XPA", "device_name": "desk-key"}'
$ curl -H "Authorization: Bearer $TOKEN" https://hub.example/v1/sessions
[{"id": "5b9f2e81c044", "name": "shop", "machine": "server", "state": "needs_permission",
"pending": {"tool_name": "Bash", "detail": "npm test -- payments", "recommended": "2", …}}, …]
$ curl -X POST -H "Authorization: Bearer $TOKEN" https://hub.example/v1/sessions/5b9f2e81c044/approve -d '{"option": "2"}'
# or live, over one WebSocket: /v1/stream
→ {"type": "subscribe", "topic": "sessions"}
← {"type": "sessions", "sessions": [ … ]}
A key that approves
A Stream Deck key that lights when an agent waits, and sends the recommended option when pressed: POST /v1/sessions/{id}/approve.
A display of who needs you
An e-ink frame on the desk, fed by the stream’s sessions frames: state, machine and what each one asks.
Home Assistant
A sensor per session state, and an automation that turns a lamp amber while one waits on you.
Voice, anywhere
Record a reply, turn it into text with POST /v1/asr, and send it with POST /v1/sessions/{id}/input.

Ideas: the glasses app is the client muxtower ships besides its web app.

07

Light, and it shows you.

Every daemon measures itself every 5 seconds, with no outside command, and the hub gathers every machine’s numbers in the app’s Daemons page and in GET /v1/usage.

  • CPU over the last minute and the last ten, memory, threads, open files, and how long each round of reading the screens takes.
  • Over a limit (20% of a core, 200 ms a round, 500 MB), a mark by the machine’s name says what, how much and the limit. A slow round names its slowest screen rule.
  • The app measures itself too: live frames, how long a terminal takes to draw, and its memory.

Daemons · usage

MachineCPU 1 minCPU 10 minMemoryRound
nas1.8%2.1%46 MB4 ms
server0.9%1.2%38 MB3 ms
vps1.1%1.0%41 MB3 ms
macbook24.6%9.8%52 MB230 ms
pc1.4%1.6%44 MB5 ms

macbook: tick 230 ms (limit 200): slowest: rule tool-permission on shop-research, 190 ms

08

Update the hub. The rest follow.

Upgrade the one machine that is always on. On every link it tells each machine joined to it which version it runs.

  • A machine too old to talk to its hub installs the hub’s version from PyPI (pipx, or pip outside it) and restarts in place, with the same process id, so systemd or launchd sees no exit.
  • One that is only older is told, in muxtower and on the Daemons page. With hub.update = "always" it follows every update; with "never" it is only told.
  • It never downgrades. If PyPI does not have the release yet, it tries again ten minutes later, as long as the hub still runs it.
updating every machine
# on the hub, and only there
$ pipx upgrade muxtower
# on a machine that should follow every hub update: ~/.config/muxtower/config.toml
[hub]
update = "always" # default: only when it must; "never": only tell me

09

Safe on the open internet.

muxtower brings its own lock, and fits behind whatever you already run.

A token per device
Each phone, browser and machine has its own, revocable on its own.
One-time pairing codes
Single use, five minutes, rate-limited. Moving to a new phone is one QR code, not a copied token.
Bans the guessers
Only a wrong credential answers 401 or 403, so fail2ban or your WAF can ban whoever is guessing.
Your own sign-in in front
Cloudflare Access, an SSO proxy or a password gate: one sign-in covers the app, the API and the stream.
No muxtower cloud
The daemon is yours. Your code stays on your machines; this site is static and never sees a token.

10

Running in two minutes.

Python 3.11+ and tmux on the machine your agents run on. The daemon serves the app itself.

on the machine your agents run on
$ pipx install muxtower
$ muxtower run claude
$ muxtower expose tailscale
$ muxtower pair

No domain and no open port: only devices on your tailnet reach the daemon.

Open the app

Pair it with your own daemons. Your daemon serves this same app itself; using that one means trusting no one else’s hosting.

Open the app

Questions