Skip to content

Repository files navigation

manymux

Persistent terminal sessions on every machine you already ssh into.

Start something long-running on a remote box, close the laptop, and pick it up later from a different network. The session survives because the machine it runs on owns the terminal, not your connection. One listing covers every machine, and when a session wants attention it reaches the machine you are sitting at.

curl -fsSL https://raw.githubusercontent.com/rayfish/manymux/master/install.sh | sh

Run that here. The machines you reach over ssh get it when you first name one: mm new gpu-box offers to install it there, and mm setup gpu-box does it ahead of time. There is nothing else to set up: no pairing, no keys to exchange, no hosts to register. If you can ssh it, you can mm it.

A first session

Start something on another machine and attach to it:

mm new gpu-box claude

Press Ctrl-] then d to detach. Claude keeps running on gpu-box. Close the laptop, go somewhere else, then look at what you have:

$ mm ls
TARGET          TITLE                       ATTACHED  IDLE  BELL
gpu-box/claude  claude: rename the project  ○ -       4m    *
gpu-box/build   cargo watch                 ○ -       12m
api             uvicorn app:api             ● 1       0s

TARGET is the thing to type next. Pick one back up:

mm attach gpu-box/claude

The screen comes back as you left it, not as scrambled scrollback. The * under BELL is that session having asked for you while nobody was watching.

Starting sessions

mm new                                   # login shell, here
mm new claude                            # run claude, here
mm new gpu-box                           # login shell on gpu-box
mm new gpu-box claude                    # run claude on gpu-box
mm new -d gpu-box cargo test             # start it, but stay where you are
mm new -n api gpu-box uvicorn app:api    # name it yourself

The first argument is where to run if it is a machine, and what to run if it is a command: a bare word found on your PATH is a command, anything else is an ssh destination. So mm new claude runs claude and mm new gpu-box gets you a shell there. For a program that shares a name with one of your hosts, mm new local <cmd> forces this machine.

Sessions are named after the command unless you pass -n. Using a machine also starts listing it.

Detaching and coming back

Ctrl-] then d detaches. Not tmux's Ctrl-b or screen's Ctrl-a, because you are quite likely running one of those inside a session: manymux has no panes, so splitting a window is still their job, and taking their prefix would mean swallowing it. Set MM_PREFIX to change it:

export MM_PREFIX=C-b        # or ^B, or \x02

While you are attached, a dim ● host/name sits in the bottom-right corner and the window title is prefixed with mm, so a session is never mistaken for a plain shell. The row keeps to itself: the session is told the screen is one row shorter, so nothing it draws lands there. Detaching gives the row, the title and the terminal back.

If the connection drops rather than you leaving, the client waits instead of dropping you back at your shell. The session is still running on a machine that never noticed, so a wifi hop or a closed lid just puts a line on that row and the screen comes back as it was:

not answering, retrying in 4s  ctrl-c to stop

It keeps trying, quickly at first and then every ten seconds, and it never stops on its own: the session is still running on a machine that never noticed you left, so a laptop shut for the weekend opens on the session you were in. Giving up is yours to do, with Ctrl-C or Ctrl-] then d.

What it goes back to is the session you were in when the connection went, not the one you started on: if you attached with mm a gpu-box and tabbed on from there, that is where it puts you.

Watching without typing

mm view gpu-box/claude

The same screen an attach gives you, scrolling and search included, with the keyboard going nowhere. The mark is a hollow grey rather than a green dot, so a window you cannot type into never looks like one you can.

The node is what enforces it, not the client, so this is safe to point at a session somebody else is working in: their keystrokes are theirs, and yours are dropped at the far end rather than on trust. A viewer also stays out of the size negotiation, so watching from a phone or a narrow split cannot reflow the screen of whoever is working. mm view on a machine running a manymux too old to promise any of that says so instead of attaching.

Copying out of a session is your terminal's job, over OSC 52, which manymux passes through untouched. Most terminals refuse clipboard writes until you say otherwise (iTerm2: Settings > General > Selection; Terminal.app has no OSC 52 at all). A session is drawn on a screen of its own by default, so mouse selection covers what is on screen and not what has scrolled past it. See Scrollback for the other way round.

Targets are host/name for another machine and a bare name for this one. A bare name is looked for here first, then across every machine, so this finds the session wherever you left it:

mm attach api

Pasting an image

Ctrl-V pastes the image on your clipboard into a session on another machine. The client reads the clipboard here, sends the bytes to the host, and what the program receives is the path they were written to, which is what claude wants: it reads the file itself.

Nothing is installed for it on the far machine. On yours it uses what is already there: AppleScript on macOS, wl-paste on Wayland, xclip on X11. Copying an image file in a file manager works too. The key is only taken when there is an image to send, so Ctrl-V still reaches the session otherwise; MM_PASTE=off gives it back for good. Pasted files land in /tmp/manymux-<uid>/pastes on the host and are cleaned up after a day.

If it says the host is too old to take pasted files, that is the node holding the session, not the binary you typed into: see Keeping it running.

Modes

Modal, like vim, and for the same reason: the keys worth having are the ones a session wants for itself.

Focus is where you live. Every keystroke is the session's, and the row at the bottom reads ● host/name with the dot green.

Control is where the keys are the client's. Ctrl-] gets you there, the dot goes hollow and amber, and the row spells out what the keys do. It stays on, so one Ctrl-] then tab tab tab walks through your sessions.

tab next session on this machine
shift-tab previous
h next machine
H previous machine
l the one you came from
1-9 the sessions you have been in, most recent first
n start a session on this machine and go to it
r rename this session
d detach
esc, enter, Ctrl-] back to focus

The digits are the same trail l walks, all of it: the box numbers the sessions this attach has been in down its left edge, 1 being the one you are in and 2 the one you came from, and pressing one goes there. It is the order you have been in them rather than the order they are listed in, so 2 is the way back whatever you have done since, and a session that ends takes its number with it rather than leaving a hole. Sessions you have not been in this run wear no digit and are a tab away. Nothing on disk remembers any of this: a fresh mm attach starts with a trail of one.

n starts a shell on the machine you are on, the way mm new would, and puts you in it in focus mode: it is the one control key that does not leave the mode on, because what follows a new session is typing rather than another hop. The node picks the name, and mm ls has it from then on.

Ending a session you moved to puts you back in the one you came from, rather than back at your shell: type exit in the session n just started and you are where you pressed it, with the row saying what ended and with what status. Only the session you named on the command line ends the attach when it exits, so mm attach gpu-box/build; echo $? still tells you what the build did.

r opens a prompt on the mark row: type a name, enter renames the session, esc leaves it alone. It is the same rename mm rename does, so the mark and mm ls say the new name from then on, and the row says so if the host refused it because another session is already called that.

Two levels, because that is how your sessions are arranged. tab stays on the machine you are on and wraps around its sessions; h moves you to the next machine you watch and lands on its first session. Both go in the order mm ls prints them. Ctrl-] twice in quick succession, within three seconds, also sends one through to the session, for whatever wants it in there. Slower than that it is just a look at the mode and a way back out, and nothing reaches the session. Any other key drops back to focus and goes through, so a mistyped mode key costs you a stray keystroke rather than a swallowed line.

About that key

Ctrl-] is picked for being one every terminal sends without being asked, and one nothing much else wants. Two near misses, in case you were about to suggest them:

  • Ctrl-Space is taken by input switching: macOS binds it to the next input source once you have two, and fcitx5 and ibus do the same on Linux. It never reaches the terminal at all.
  • Ctrl-` looks like it should work, since clearing the top bits off a backtick gives the same NUL as Ctrl-Space. Terminals only do that masking for @, A-Z, [, \, ], ^, _ and space, and the backtick is outside the set, so you get a plain backtick and nothing happens.

What does want Ctrl-] is vim's jump-to-tag and telnet's escape. Press it twice quickly to send one through, in there or anywhere else. MM_PREFIX takes any of the above spellings (C-], C-b, ^B, C-Space, or the raw byte).

To see what your terminal sends for a key, run cat -v outside a session and press it: Ctrl-] shows up as ^].

Machines

Any ssh destination works, including the aliases, ProxyJump hosts and tailscale names in your ~/.ssh/config. Starting a session on a machine adds it to the listing; mm add is for machines you only want to watch:

mm add deploy@prod-1
mm hosts
mm rm deploy@prod-1

A machine is listed and addressed by the destination exactly as you added it, so deploy@prod-1 gives you targets like deploy@prod-1/api, and that same spelling is what mm rm wants. A short Host alias in your ~/.ssh/config keeps the column narrow.

Ending sessions

mm kill gpu-box/build     # SIGHUP to the process group

Exiting the shell inside a session ends it too, and it drops out of the listing on its own.

Names

A session is addressed by host/name, and the name is yours to change:

mm rename gpu-box/zsh-3 build

From inside the session it is Ctrl-] r, then the name, then enter. Names go in paths and in the terminal, so they keep to letters, digits, -, _ and .: a space becomes a dash and anything else is dropped, and what you end up called is what the mark row and mm ls show. A name another session on that machine already has is refused rather than made unique behind your back.

Titles, bells and notifications

Sessions name themselves. Whatever the program sets as its terminal title (OSC 0/1/2) shows up in mm ls, so a Claude Code session appears under whatever it is working on. That column follows the program and nothing else: the name is what you set, the title is what it is doing.

When a session rings the bell or asks for a notification outright (OSC 9, OSC 777) and nobody is attached to see it, you are told. Where depends on where you are sitting:

  • Attached to a session on that machine, a bell in one of its other sessions goes to the terminal in front of you, as an OSC 9 that iTerm2, kitty, ghostty, WezTerm, foot, VS Code and Windows Terminal raise as a desktop notification. The status row names the session for a few seconds as well, for a terminal that raises nothing. This is the route that works over ssh, where a notification on the far machine would be raised on a desktop nobody is at.
  • Attached nowhere on it, whichever machine watches it says so on its own desktop: osascript on macOS, notify-send on Linux.

Somebody attached on a machine is enough for the desktop notifier to keep out of the way, so one bell interrupts once. A session someone is watching stays quiet altogether, and one session can interrupt you at most every 30 seconds. A clean exit is not worth interrupting over; a non-zero one is.

To turn the lot off:

mm config notify off

It takes hold at once, in the session you are already attached to and in any node already running. MM_NOTIFY=off does the same for one command, and mm daemon --no-notify runs the node but stays silent.

Scrollback

Attaching takes a screen of its own, the way tmux does, so your terminal's scrollbar still shows the shell you attached from and the wheel has nothing of the session's to move. If you would rather your terminal kept the session's history:

mm attach --screen inline web01     # this attach only
mm config screen inline             # from now on

Inline paints on the terminal's own screen, so what the session prints scrolls into your terminal's scrollback, and the wheel, the find bar and selection are your terminal's own. Attaching also brings the last thousand lines the session printed while you were away, so a build you were not there for is there to scroll. A full-screen program in the session (vim, less) reaches for the terminal's alternate screen itself, exactly as it would over plain ssh.

The catch is that the mark on the bottom row is fenced off with a scrolling region, and a terminal that throws away what scrolls out of one keeps nothing. iTerm2 keeps it. If yours does not, --screen alternate is the way back.

On the alternate screen the terminal has no scrollback to offer, so manymux shows you its own. Ctrl-] [ opens it, pgup/pgdn, g and G move it, and esc goes back to the live session.

The mouse is your terminal's, so dragging selects and copies exactly as it does anywhere else, with no modifier held. That is why the wheel does not open the view: reading the wheel means asking the terminal to report the mouse, and a terminal reporting the mouse is not selecting with it. If you would rather scroll with the wheel than select with a bare drag, ask for it:

mm config mouse client

Then the mouse is manymux's for the whole attach. A notch opens the view and moves it, and a drag selects: press, drag, let go, and what you dragged over is on your clipboard. Double click takes the word under it, which counts a path or a URL as one word, and triple click takes the line. Hold the drag against the top or bottom of the window and the view moves under it, a line at a time to begin with and faster the longer you hold, so a selection is not limited to what happens to be on the screen. Letting go at the live screen gives it straight back, so the session never appears to stop; scrolled back, the highlight stays where you left it and so does the view.

The copy goes to your terminal over OSC 52, which most terminals refuse until you say otherwise (iTerm2: Settings > General > Selection). Without that the highlight works and nothing lands on the clipboard, and there is no way for manymux to tell.

Nothing here is taken from a program that asked for the mouse itself: Claude Code, vim and htop keep every report, wheel and drag alike, under either setting. Inside one of those, selecting is that program's business exactly as it is over plain ssh.

Ctrl-] / searches everything the session has printed, all ten thousand lines of it. n walks back through the matches and N comes back towards the live screen. Lowercase ignores case; a capital means it. Every match comes back in one answer, so walking them costs nothing even on a machine two hops away.

None of this exists inline, and none of those keys are taken there: your terminal's own scrollbar and find bar are already looking at the same lines, and they are better at it.

Keeping it running

The node starts on demand, the way tmux starts its server, so nothing has to be running for mm new to work. To have it survive logging out and keep watching for bells while you are away:

mm service install

That writes the right unit for launchd, systemd, OpenRC, SysV init or rc.d. Run as yourself on launchd or systemd and you get a per-user unit with no root involved. Run it with sudo, or on a manager with no per-user services, and it becomes a system unit naming the account it runs as.

Keeping the binary current, on each machine:

mm update --check
mm update
mm update --nightly   # master's last build, rather than the newest release

--nightly is not remembered: a plain mm update afterwards puts the release back, which goes backwards whenever master is ahead of the tag.

Replacing the binary is only half of it. The node is a long-running process still executing the one it started from, so until it restarts the machine keeps behaving like the old build: a client that pastes images into a session hosted by an older node gets told the host cannot take one, however current its own binary is. mm update asks the node what it is running and offers the restart when the two differ, including when there was nothing to download. Every session dies with the node, so it asks first, and --force answers in advance:

mm restart          # asks, if there are sessions to lose
mm restart --force  # does not

Run from a script or over ssh there is nobody to ask, so it says what the restart would cost and leaves it. mm stop and mm start are the same thing in halves, for a machine to leave quiet or one to have ready before anything asks.

Waiting for every session to be idle at once is not a thing that happens, so the sessions can be written down and started again instead:

mm update --keep-sessions

That is three steps in one: what each session is doing goes into checkpoint.toml, the node restarts, and the sessions are started again where they were. A session it could not describe stops the restart rather than being quietly dropped, so it never costs work it did not write down; adding --force says to go ahead and lose those, and says which they are.

Run it from a shell that is not a session. A plain ssh in is fine; a session is not. The restart hangs every session up, and a client typed inside one is in that session's process group, so it would be killed halfway through: the checkpoint written, the node gone, and nothing left running to put anything back. It refuses rather than letting that happen, and says so.

The three by hand, which is what to reach for when only one machine needs it:

mm checkpoint save            # every machine, or --host one
mm checkpoint show            # what is written down; asks no machine anything
mm checkpoint restore         # start them again; --dry-run to look first

What is recorded is the directory each session was working in and the command running in it. A program that knows how to pick up where it left off is asked to: claude and pi both come back with --continue, resuming the conversation that was open rather than starting a new one. Anything else is run again as it was, and a session sitting at a prompt comes back as a login shell in the same directory. The file is plain TOML and is meant to be edited before you restore it.

One that was already resuming keeps whichever conversation it named, so claude --resume 7f3a… comes back on that one. A bare --resume is the exception: it opens a chooser and waits for somebody to walk it, which a restore has nobody to do, so it is written down as --continue instead and the session comes back on the conversation that was open in that directory.

show and restore --dry-run put the line each session will actually be started with under it, since what runs is the captured command inside a wrapper that keeps the login shell behind it:

gpu-box/build @work claude --continue in /srv/project
  exec sh -mc '"$@"; exec "${SHELL:-/bin/sh}" -l' sh claude --continue

Four things worth knowing:

  • Groups belong to the client, so take the checkpoint from the machine you work at. Membership is keyed on a pid and a restore makes new ones, so the grouping is rebuilt from the file afterwards — and only the machine holding your groups.toml knows what it was. Run on the host, --keep-sessions brings the sessions back but knows nothing of a grouping made at your desk.
  • Restoring twice is safe. A name that is already running is left alone, so a host that put its own sessions back and a client restoring its groups compose: save at your desk, mm update --keep-sessions on the box, then mm checkpoint restore at your desk to put the groups back.
  • Reading this needs Linux, where the directory and the command come out of /proc. A machine that cannot say is named rather than guessed at, and its sessions are left out of the file: put back in the wrong directory, a program told to resume picks up somebody else's work.
  • A machine whose node is too old to be asked is read directly instead. Asking a node what its sessions are doing is a request it only learned in the build that added this, and the node holding the sessions worth saving is, the first time round, always older than that. So when it refuses, the files it would have read are read over the ssh connection that is already open. Nothing on the far side needs to have heard of checkpoints, or of manymux at all: the far end runs cat and readlink, and every rule about what that means is applied at your end, once. It costs a second round trip per machine, which a shared ssh connection makes a message rather than a handshake.

The sessions do go, but they are hung up first and given a couple of seconds, so shells write their history and editors write their swap files rather than finding the terminal gone mid-write. Whatever is still running when that time is up is killed rather than left on a terminal that no longer exists. The same happens on SIGTERM, so stopping the service or rebooting is no more abrupt than mm stop.

The machine that has to be current is the one the session lives on, which for a remote session is the far end: ssh gpu-box mm update.

Nightly builds all share one version number, so mm --version names the commit too:

mm 0.1.0 (a1b2c3d4)

That is the string to compare when you want to know whether two machines are actually running the same thing.

Commands

mm ls [host]                         list sessions, everywhere or on one machine
mm new [host] [-n name] [-d] [cmd]   start a session (default: your login shell)
mm attach <target> [--screen ...]    attach; Ctrl-] then tab switches, d detaches
mm view <target> [--screen ...]      watch a session without typing into it
mm kill <target>                     SIGHUP a session's process group
mm rename <target> <name>            give a session a different name
mm add <host> | hosts | rm <host>    which machines to list and watch
mm config [key] [value]              show or change a setting: notify, screen, mouse
mm update [--check] [--nightly]      replace this binary with the published one
mm checkpoint save|show|restore      write down what every session is doing
mm start | stop | restart [--force]  this machine's node: up, down, and again
      restart|update --keep-sessions write a checkpoint and put them back after
mm service install|uninstall         run the node at boot
mm completions [shell] [--install]   tab completion, with your session names in it

The ones you type often have a short form: l, n, a, v, k, r, h, up.

Two more exist and are hidden from mm --help, because nothing types them: mm daemon is the node itself, run by the service unit or by a client that found none running, and mm agent is what ssh <host> mm agent runs. See How it works.

Tab completion

mm completions --install writes the script where your shell looks for it, and mm update installs it if there is none and rewrites it if there is, whether that update downloads anything or not, so a machine set up with install.sh picks it up the next time it looks. It is a stub that asks mm on every tab, so mm a <TAB> offers the sessions running right now:

$ mm a <TAB>
api  build  gpu-box/  laptop/

$ mm a gpu-box/<TAB>
gpu-box/train  gpu-box/logs

A bare tab stays on this machine and offers host/ as a way in, because a keystroke should not wait on ssh. Only naming a machine goes out to it, and it gives up rather than hanging if that machine is asleep.

For zsh, --install asks zsh which directories it autoloads from and writes to the first one you can write in: $PREFIX/share/zsh/site-functions on Termux, /usr/local/share/zsh/site-functions on a Homebrew mac. Nothing needs adding to any file in that case, and nothing is said beyond where the script went.

Where none of them is yours, which is the usual case on a shared Linux box, the script goes to ~/.local/share/zsh/site-functions and that directory has to go on the fpath before compinit runs:

fpath=(~/.local/share/zsh/site-functions $fpath)
autoload -Uz compinit && compinit

mm prints those two lines once, and stops printing them once .zshrc names the directory. If it is a line you would rather not add, source the script instead, at the end of ~/.zshrc:

echo 'source <(mm completions zsh)' >> ~/.zshrc

bash under Termux is the other exception: it searches $PREFIX rather than $HOME, so its script goes there too, and it reads any completion directory only once pkg install bash-completion has happened, which is not part of the Termux base and is not something mm can do for you.

Verifying a release

Every release publishes <asset>.sha256 beside the binary, and a signature over that sidecar as <asset>.sha256.sig: Ed25519, hex encoded. The chain is the ordinary one, and both halves count. The signature says the sidecar is ours, and the sidecar says which binary it was written for.

mm update checks both by itself, with no tooling on the machine, and refuses to install a signature that is not ours. To check one by hand:

curl -fsSLO https://github.com/rayfish/manymux/releases/latest/download/mm-linux-x86_64.sha256
curl -fsSLO https://github.com/rayfish/manymux/releases/latest/download/mm-linux-x86_64.sha256.sig
xxd -r -p mm-linux-x86_64.sha256.sig > sig.bin
openssl pkeyutl -verify -pubin -inkey release.pem -rawin \
    -in mm-linux-x86_64.sha256 -sigfile sig.bin
sha256sum -c mm-linux-x86_64.sha256

The checksum on its own catches a download that arrived wrong, and nothing more: it comes from the same place as the binary, so anything that can replace one can replace the other. The signature is what says a release was published by somebody holding the key, which the release host does not have.

Install details

Reaching a machine runs ssh host mm agent through a non-interactive shell, which reads neither .zshrc nor .bashrc. Where mm has to live so that shell can find it differs by platform, and the installer handles both.

On Linux it goes in /usr/local/bin, taking sudo if this account has it for free. That is not fussiness: /usr/local/bin is on the PATH sshd hands out there, and ~/.local/bin is invisible to it. An account that would have to type a password gets ~/.local/bin instead of a prompt, since a deploy user reached by key has no password to type and no sudoers entry either. A client that gets nothing from a plain mm tries ~/.local/bin/mm next, so such a machine is still reachable, at one wasted ssh per connection. curl ... | sudo sh puts it in /usr/local/bin and saves that.

On macOS it goes in ~/.local/bin, no sudo, and the installer puts that directory on your PATH in ~/.zshenv. macOS gives /usr/local/bin no such advantage: it reaches an interactive shell only through path_helper in /etc/zprofile, which a non-interactive zsh -c never reads, so a system install would cost a password and still leave the machine unreachable. ~/.zshenv is the file that works, being the one zsh reads on every invocation. MM_SKIP_PATH=1 leaves it alone if you would rather do it yourself.

On Android the same line works inside Termux, which gets its own binary: mm there is linked against bionic, so neither Linux build runs. It lands in $PREFIX/bin, which is already on PATH and needs no root. pkg install curl first if the installer says so.

A phone is a client like any other machine, so mm ls and mm attach gpu-box/x work from it. Sessions started on the phone are the one difference: they last as long as Termux does, and termux-wake-lock is what stops Android freezing it in the background. There is no service to install, since Android has none.

INSTALL_DIR overrides the location and MM_VERSION pins a release.

There is also an Android app, in android/. It is not a copy of the CLI: it makes its own ssh connection in the process, emulates the session's screen on the phone, and has no mode key, since a phone has chrome a terminal has not and every verb behind Ctrl-] can be something you press instead. It is early, and android/README.md says what is in it. Releases carry a manymux-android.apk built from the same commit as the binaries beside it. It is debug-signed for now, so installing a newer one over an older one may want the old one removed first.

Environment

MM_PREFIX control-mode key, e.g. C-b. Default Ctrl-]
MM_PASTE off gives Ctrl-V back to the session
MM_SSH the ssh program, if yours lives somewhere unusual
MM_LOG log filter, e.g. manymux=debug
MM_NOTIFY off silences bells for one command; see mm config
MM_CONFIG_DIR where the host list and settings live
MM_COMPLETE_REMOTE let a bare tab reach every machine, not just this one
NO_COLOR plain output; CLICOLOR_FORCE colours a pipe

The CLI logs to stderr. The node also writes a daily-rotating file, keeping seven: ~/Library/Logs/manymux on macOS, $XDG_STATE_HOME/manymux elsewhere.

Why not tmux

Tmux keeps the session alive, which is most of the job, but it is server-local: tmux ls only lists the machine you already logged into, so five boxes are five separate worlds. And when something finishes on a machine you are not looking at, nothing tells you.

manymux keeps the sessions and adds those two: one listing across every machine, and events that reach the machine you are actually sitting at. It has no panes, tabs or splits, because tmux is still there for that, inside a session.

How it works

  laptop                          gpu-box
  mm ls, mm attach                mm daemon (one node per user, unix socket)
        |                         |
        +-- ssh gpu-box mm agent -+
                                  |
                                  +-- session "api"   -> PTY -> claude
                                  +-- session "build" -> PTY -> cargo watch

A node is one mm daemon process. It owns that machine's sessions and listens on a Unix socket; that is all.

  • No networking of its own. Reaching a machine is ssh <host> mm agent, using your ~/.ssh/config, so ProxyJump, tailscale names and jump hosts all work unchanged. Connections are shared (ControlMaster), so the second command to a host is a round trip rather than a handshake.
  • No second ACL. sshd decides who gets in with whatever you already configured. manymux keeps no allowlist, no invites and no keys, so there is nothing to drift out of sync and no way for it to grant access ssh would refuse.
  • The node owns the PTY, so a client leaving is invisible to the child: no SIGHUP, no EOF. That is the whole trick.
  • One node per user, because the socket is named from the uid alone (/tmp/manymux-<uid>) and from nothing about the login. ssh deploy@box lands in deploy's node, and your own sessions are the same ones whether you came in over plain ssh, a mesh, a phone, or the keyboard in front of you. manymux never switches users or runs as root; ssh already put the request in the right account.
  • Reattaching repaints from a headless terminal emulator (avt) fed every output byte, so htop and vim come back intact. Mouse reporting, bracketed paste, cursor style and the window title are tracked separately and replayed after it, since the screen model has no opinion on those.

Build

cargo build --release
cargo test        # includes end-to-end tests through a stub ssh

cargo build --lib --no-default-features is the client core, without PTYs or a terminal: what a mobile app links against, and it has to keep compiling.

Known gap: a program that queries the terminal (cursor position, background colour) while nothing is attached gets no answer, because there is no terminal to ask.

License

MPL-2.0.

About

Persistent terminal sessions you can leave and come back to, on every machine you already ssh into

Resources

Stars

6 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages