TRUSTMUX(1)

NAME
trustmux - manage the Trustmux daemon

SYNOPSIS
trustmux COMMAND [options]

DESCRIPTION
trustmux is the single command-line interface for the Trustmux daemon — a lightweight server that lets you monitor and interact with your tmux/Byobu sessions from a phone browser over your Tailscale network.

All operations are subcommands of trustmux: initial setup, starting and stopping the daemon, login-hook management, and device pairing.

COMMANDS

  Daemon lifecycle

setup
    One-time setup: verify Tailscale is installed and connected, then configure tailscale serve for HTTPS on the daemon port (7432 by default; see --port).

start
    Start the daemon in HTTPS mode via tailscale serve (default, recommended).

start-local
    Start the daemon bound to 127.0.0.1 only, for access via an SSH port-forward. No TLS certificate is generated; the tunnel provides encryption.

start-direct
    Start the daemon with a self-signed TLS certificate, binding on all interfaces for LAN/direct access without Tailscale.

stop
    Send SIGTERM to the running daemon. Tailscale serve configuration is left in place.

restart
    Stop then start the daemon in HTTPS mode.

status
    Show whether the daemon is running and the URL to connect to.

log Tail this instance's daemon log (see FILES).

list
    List every instance that has a state directory, with the port, scheme and pid of any that are running. Instances whose daemon is stopped are shown with dashes.

rm  Delete this instance's state directory — session tokens, TLS keypair, log, socket and pid file — and remove its login hook so it does not start again at the next login.

    An instance is created by its first start and persists until removed, so this is the only way to retire one. Refused while the instance's daemon is running, and refused for the default instance; --force overrides both, stopping a running daemon first.

  Login hook

enable
    Run first-time setup if needed, start the daemon, and install a hook in the user's shell login file (~/.profile, ~/.bash_profile, or ~/.zprofile) so the daemon starts automatically on each login. Only available for the default instance, since it starts the daemon in tailscale serve mode; see --name.

disable
    Stop the running daemon and remove the login hook so the daemon no longer starts automatically. Only this instance's hook is removed, so disabling one leaves any others in place. Paired device tokens are preserved.

  Device pairing

pair
    Ask the running daemon to generate a one-time 6-digit pairing code (valid for 60 seconds) and display it along with the URL your phone should open. If qrencode(1) is installed, a QR code is printed for one-scan pairing. The command then waits for the code to be used and returns as soon as a device pairs, or when the code expires, whichever comes first. It clears the screen and prints either pair accepted from IP (exit status 0) or no client paired (exit status 1).

unpair
    List all currently paired devices (IP address, browser label, and pairing time) and offer an interactive menu to remove one or all of them.

OPTIONS

setup --quiet
    Suppress the "Next steps" output after a successful setup.

--port PORT
    TCP port the daemon listens on (default: 7432). Accepted by setup, start, start-local, start-direct, stop, restart, status and enable.

    Only the port changes; each mode binds the same address it always has. stop and status do not normally need the flag: they ask the running daemon over the admin socket which port it is on, so a daemon started on a non-default port is still found without repeating --port. The same answer gives pair the correct URL and QR code.

    enable records the port in the login hook it writes, so a non-default port survives logout; re-running enable --port rewrites an existing hook rather than leaving the old port in place.

--advertise SOURCE
    Address a phone should use, for when it is not the one this host can see. Accepted by start, start-local, start-direct, restart and enable. Repeatable.

    Behind NAT — a cloud instance, most often — the daemon only ever sees an internal address, so both the URL pair prints and the names in the certificate start-direct generates describe a host nothing outside can route to. SOURCE overrides that. It does not change what the daemon binds; combine it with a firewall rule that admits the port.

    SOURCE is one of:

    HOST
        or HOST:PORT A literal address or name. The daemon's own scheme and port fill in what is omitted, so --advertise 203.0.113.45 on a daemon serving HTTPS on 7432 advertises https://203.0.113.45:7432/.

    URL A full http:// or https:// URL, which overrides both scheme and port — for a reverse proxy terminating TLS on 443 in front of the daemon. A path, query or fragment is rejected: trustmux serves from the root, and the pairing code is carried in the fragment.

    cmd:PROGRAM
        Run PROGRAM and take one address per line of its standard output.

    cmd: is how an address that changes stays correct. A cloud instance with an ephemeral public address gets a different one across a stop/start cycle, so a literal written down once would quietly go stale, while cmd: is re-run on every daemon start.

    PROGRAM is split into arguments with shell-style quoting but is not run by a shell: no pipelines, no redirection, no $(...). Those characters are rejected rather than passed through to the program, so put a pipeline in a script and name the script. An absolute path is safest, a daemon's PATH being whatever started it.

    Resolution is strict, and any failure stops the daemon rather than falling back to the address it would have guessed: a non-zero exit, no output, a blank line, a value that is not an address, or more than 5 seconds across all sources. This is deliberate. A shell wrapper whose curl fails typically prints an empty line and still exits 0, and treating that as "nothing to advertise" would hand out an unreachable URL and a certificate for the wrong host — neither of which a warning would fix.

    Every resolved address is added to the certificate as a subject alternative name; the first supplies the URL that pair, status and start print. A browser rejects a certificate that omits the name in the URL bar outright, rather than offering the click-through a self-signed certificate gets, so advertising an address without certifying it would be worse than useless.

--no-advertise
    Advertise nothing, ignoring any source in TRUSTMUX_ADVERTISE or the instance's configuration file. Accepted wherever --advertise is.

--name NAME
    Act on the named instance instead of default. Accepted by every subcommand. Each instance keeps its own pid file, admin socket, log, session tokens and TLS certificate, so several daemons can run at once — on different ports, or on the same port at different bind addresses.

    NAME must match `[A-Za-z0-9][A-Za-z0-9._-]{0,31}': it becomes a directory name and is written into the login hook, so separators, leading dots and shell metacharacters are rejected rather than sanitised.

    Only the default instance can use tailscale serve mode. serve publishes on the tailnet's port 443, which just one daemon can own — a second would silently take over the mapping and leave the first running but unreachable. Named instances therefore use start-direct or start-local, and setup, start, restart and enable refuse them with a message pointing at those modes. Note that --port does not lift this restriction: it moves only the loopback backend that tailscale serve proxies to, not the tailnet-facing port.

--force
    Accepted by rm. Remove the instance even though its daemon is running (it is stopped first), or even though it is the default instance.

ENVIRONMENT

TRUSTMUX_PORT
    Default port when --port is not given, for users who always want a different port. An out-of-range or non-numeric value is an error, not a silent fallback.

TRUSTMUX_INSTANCE
    Default instance when --name is not given. An invalid name is an error, not a silent fallback.

TRUSTMUX_ADVERTISE
    Default advertise source when --advertise is not given. Holds a single source, not a list: no separator would be safe, since `:' collides with both URL schemes and the cmd: prefix while `,' can appear inside a command's arguments. A cmd: source already yields as many addresses as it likes, so nothing is out of reach.

TRUSTMUX_CONFIG_DIR
TRUSTMUX_STATE_DIR
    Override the configuration and state base directories individually. Each takes precedence over the corresponding XDG variable. Pointing both at a scratch directory is the supported way to run a throwaway daemon — from a development tree, for instance — without touching the real one. start , stop , status and list act only on a daemon that this instance itself started, determined from its own admin socket and pid file rather than from who holds a port, so the isolation holds even when the scratch daemon shares a port with the real one.

FILES
Files live in the XDG base directories, with one subdirectory per instance: $XDG_CONFIG_HOME (default ~/.config) holds the one file the user writes by hand, and $XDG_STATE_HOME (default ~/.local/state) holds everything the daemon owns.

Below, INSTANCE is the --name name, or default when unset.

  Configuration

$XDG_CONFIG_HOME/trustmux/machines.json
    Optional: sibling machine list for the in-app machine selector. Shared by every instance.

$XDG_CONFIG_HOME/trustmux/instances/INSTANCE.json
    Optional per-instance settings, hand-written. One key so far: advertise, a string or list of strings, each a --advertise source.

    {"advertise": ["cmd:/usr/local/bin/external-ip", "tmux.example.com"]}

    This is the durable home for a source, and the only one a login hook will find: --advertise applies to one daemon and then is gone. Sources are re-resolved on every start, which is what makes writing one down safe even when the address behind it changes.

    Because a source can name a program to run, a file writable by group or other is refused rather than read; keep it mode 0600. Configuration rather than state because it records intent, not a result.

  State

$XDG_STATE_HOME/trustmux/instances/INSTANCE/tokens.json
    Paired device session tokens (mode 0600).

$XDG_STATE_HOME/trustmux/instances/INSTANCE/cert.pem
$XDG_STATE_HOME/trustmux/instances/INSTANCE/key.pem
    Self-signed TLS keypair used by start-direct.

$XDG_STATE_HOME/trustmux/instances/INSTANCE/trustmux.log
    Daemon log file, tailed by trustmux log.

$XDG_STATE_HOME/trustmux/instances/INSTANCE/trustmux.sock
    Admin Unix socket (mode 0600) used by pair, unpair and by stop and status to ask the daemon which port it is on.

$XDG_STATE_HOME/trustmux/instances/INSTANCE/trustmux.pid
    PID file written at daemon startup, holding "pid port". The port is recorded so that a daemon which has stopped replying on its admin socket can still be matched against the port stop was asked about.

The socket and pid file are deliberately kept here rather than under $XDG_RUNTIME_DIR, where the spec would put them: systemd-logind deletes /run/user/$UID when a user's last login session ends unless loginctl enable-linger is set, which would strand a still-running daemon with no socket to reach it by — and trustmux exists precisely to be started and then reached later. That directory also does not exist on macOS or in most containers. A socket or pid file left behind by a killed daemon is therefore detected rather than swept away: the socket refuses connections once its daemon is gone, and a recorded pid is only believed when it is confirmed to hold the port in question.

  Migration
Earlier versions kept everything directly in ~/.config/trustmux. On first run, tokens.json, cert.pem, key.pem and trustmux.log are moved from there into the default instance's state directory, preserving their modes. Any stale trustmux.pid and trustmux.sock are deliberately left in place, since a daemon predating the upgrade may still be serving on that socket.

EXAMPLES
Typical first-time setup:

    trustmux setup
    trustmux enable
    trustmux pair

Daily use:

    trustmux status
    trustmux restart
    trustmux log

Run on a different port; later commands find it without the flag:

    trustmux start-direct --port 3389
    trustmux status
    trustmux pair
    trustmux stop

A second daemon alongside the first, with its own tokens and log:

    trustmux start-direct --name work --port 3389
    trustmux pair --name work
    trustmux list
    trustmux stop --name work

A throwaway daemon that touches none of the above:

    export TRUSTMUX_CONFIG_DIR=$(mktemp -d)
    export TRUSTMUX_STATE_DIR=$TRUSTMUX_CONFIG_DIR/state
    trustmux start-local --port 3389

On a cloud instance whose public address the host itself cannot see. Write a program that prints it — here a one-liner against the metadata service, which unlike a shell pipeline reports its own failure:

    $ cat /usr/local/bin/external-ip
    #!/bin/sh
    exec curl --fail --silent -H Metadata-Flavor:Google \
      http://metadata.google.internal/computeMetadata/v1/instance/\
    network-interfaces/0/access-configs/0/external-ip

    $ trustmux start-direct --advertise cmd:/usr/local/bin/external-ip
    $ trustmux pair

To keep it across reboots, put the source in the instance's configuration file (see FILES) rather than in the command line, and the address is looked up afresh each time:

    mkdir -p ~/.config/trustmux/instances
    echo '{"advertise": ["cmd:/usr/local/bin/external-ip"]}' \
      > ~/.config/trustmux/instances/default.json
    chmod 600 ~/.config/trustmux/instances/default.json
    trustmux start-direct

SECURITY

  Pairing code
The one-time pairing code is a 6-digit decimal value drawn from secrets.randbelow(1,000,000), giving 1,000,000 possible values. The code is valid for 60 seconds. After three incorrect guesses the code is immediately invalidated and a new one must be generated with trustmux pair. The hard 3-attempt lockout makes brute-force infeasible: an attacker has a 0.0003% chance of success per pairing window, and each new window requires the machine owner to run trustmux pair again. An incorrect guess also incurs a 0.5-second artificial delay.

  Session tokens
After a successful pairing the daemon issues a 256-bit URL-safe session token (secrets.token_urlsafe(32)) stored in this instance's tokens.json (mode 0600; see FILES) and delivered to the browser as an httponly, SameSite=Strict cookie. Tokens are validated with a constant-time comparison to prevent timing attacks. Sessions expire after 90 days of inactivity and can be revoked at any time with trustmux unpair.

  Network exposure
In the default start mode the daemon binds only to the Tailscale IP (100.x.x.x), so the pairing endpoint is unreachable from the public internet or from other local-network devices. In start-direct mode the daemon binds on all interfaces; the pairing-code and session-token protections above remain in force, but the network attack surface is broader. --advertise does not itself widen this — it changes only what is published — but advertising a public address is normally paired with a firewall rule admitting the port, and that does: the daemon then faces the internet with the pairing code and session token as its only protection, rather than a local network. Prefer start or start-local where either will do. In start-local mode the daemon binds to 127.0.0.1 only and is accessible solely through an explicit SSH port-forward.

SEE ALSO
trustmuxd(1), tailscale(1), tmux(1), byobu(1)

AUTHOR
Dustin Kirkland <kirkland@ubuntu.com>