tuimote / docs

Install & setup

Requirements on the host (the always-on machine your agents run on): macOS or Linux, tmux, git, and at least one supported coding agent. Clients need nothing but a modern browser.

curl -fsSL https://tuimote.org/install.sh | sh

The installer downloads the signed release binaries, verifies them, checks dependencies, and hands off to tuimote setup. Or use Homebrew:

brew tap tuimote/tap https://codeberg.org/soundworker/homebrew-tuimote
brew install tuimote/tap/tuimote
tuimote setup

Or build from source (repo): cargo build --release produces tuimote-server (the host) and tuimote (the CLI).

tuimote setup is a one-shot, re-runnable bootstrap: it runs the doctor checks, installs and starts the background service (a launchd user agent on macOS, a systemd user unit with lingering on Linux), prompts for a password if the server is reachable beyond loopback, and prints your dashboard URL — by default http://<host-ip>:8787.

Password

tuimote attaches to live tmux sessions, so a network-facing instance without auth would be an unauthenticated remote shell. The server therefore refuses to bind a non-loopback address until a password is set:

tuimote config password        # prompts; or pipe it on stdin

⚠ Never expose tuimote to the public internet. It serves a live terminal over HTTP — run it on your LAN or behind a VPN (WireGuard, NetBird, Tailscale) only. The password gate is for trusted-network hygiene, not internet hardening.

Updating

tuimote update --check         # report only
tuimote update                 # apply

Behavior follows the install method: script installs download the signed release, verify it (minisign + SHA-256), swap atomically, and restart the service. Homebrew installs print brew upgrade tuimote && tuimote setup, which refreshes and restarts the service; source builds are left to your own build loop.

What changed between your version and the latest lives on the release notes page, in the dashboard's settings modal while an update is pending, and in the CLI: tuimote changelog --since <your-version>.

Uninstall

tuimote uninstall          # remove the service + installer-managed binaries
tuimote uninstall --purge  # also remove global config + state

Homebrew and source-built binaries are left to their package/build workflow. Per-repo .tuimote/ directories are always listed and preserved.

Core concepts

The pieces

One small Rust binary per machine. tuimote-server owns the tmux sessions your agents run in and serves the dashboard, the browser terminal, and the HTTP API. The tuimote CLI talks to that server — it is the same tool for you scripting from a shell and for agents messaging each other. State and config live in ~/.tuimote/ (config.json, logs, panels, fonts, trust grants).

Sessions & shorthands

A session is one agent running in one tmux pane, tied to the repo it was started in. Every running session gets a stable shorthand<repo-slug>-<N>, e.g. dev-storefront-2 — which is how you (and other agents) address it:

tuimote session ls --running --pretty   # who's around
tuimote session send dev-storefront-2 "run the tests again"
tuimote session view dev-storefront-2   # read its screen, read-only
tuimote whoami                          # a session's own identity

Sessions can message each other, ask each other blocking questions (session request / session reply), read each other's screens, and hand work across harness boundaries (session copy --to codex) — including across federated machines.

Each repo can pin one session as its entrance point (tuimote repo pin): pinned sessions skip idle auto-stop and are the default target when a command names no session.

The dashboard

The web UI groups sessions into one card per repo: an activity pulse, token usage, and a one-line status per session, plus buttons to start a new agent in that repo. Click a session and you are in its real terminal — the full TUI with every keybind, not a reduced remote mode. On the phone you get touch scrolling, a key bar for the keys a TUI needs, and a slash-command composer. Repos on federated machines appear as their own cards, marked with the owning host, and are fully interactive.

Shared repo memory

Initialize a repo once with tuimote repo init. Its .tuimote/ directory becomes a harness-neutral shared store: memory/ (an indexed memory base injected into new and resumed sessions), STATE.md, and HISTORY.md (tuimote repo history appends dated entries). Harnesses with a native system-prompt hook get the memory index injected directly; the others read it through a managed AGENTS.md fallback. Repo-local and global skills are exposed to every harness through compatibility views, so switching harnesses doesn't mean maintaining copies.

Harness support

HarnessStatus
Claude CodeWell tested, wired to the full tuimote feature set.
Pi
CodexAvailable; might lack a feature or two.
OpenCode
oh-my-pi
Hermes
Reasonix

At least one harness must be installed on the host. You can run several at once, switch between them, and translate a session from one to another.

Keyboard shortcuts

Every app-level hotkey uses the Ctrl+Shift modifier — chosen because neither the browser, the OS, nor a TUI running in the session claims it. The combos work even while the terminal has keyboard focus: tuimote routes Ctrl+Shift chords to the app and passes everything else through to the session untouched. Keys are matched by physical position, so the bindings are keyboard-layout independent. The same cheat sheet is built into the app: press Ctrl+Shift+?.

Available anywhere:

ShortcutAction
Ctrl+Shift+NNew session — open the repo/agent picker
Ctrl+Shift+,Open settings
Ctrl+Shift+?Shortcuts help (Esc closes)

Inside a session's terminal view:

ShortcutAction
Ctrl+Shift+ / Previous / next running session (wraps around)
Ctrl+Shift+19Jump straight to the Nth running session
Ctrl+Shift+DDetach — back to the dashboard; the session keeps running
Ctrl+Shift+PToggle the dock panel

A few related behaviors that aren't chords:

On the phone the hotkeys don't apply; the terminal's key bar and touch gestures cover the same ground.

CLI reference

The full tuimote <subject> <verb> command tree lives on its own page, CLI reference. It is generated from the CLI's own command definitions, so it always matches --help.

Dock panels

Using them

The dock is the right-edge slide-in panel next to the terminal (the pull tab on the terminal's right edge). It hosts pluggable pages: the built-in Docs reader for artifacts, a git page, and whatever panels you or a repo add — file browsers, issue trackers, system monitors. Panels are plain JS files, discovered and hot-loaded: drop a file, reload the PWA, no rebuild.

Panels shipped inside a repo are untrusted by default: the first time a repo's panels are seen you get a one-time trust prompt, and even after that their shell access is limited to an allowlist the panel declares. Manage this with tuimote panel ls and tuimote panel trust [--revoke]. Per repo, you can also reorder and hide panels from the repo settings menu.

Writing your own

A panel is one file that registers a page object — id equals the filename stem. The smallest useful page:

// ~/.tuimote/panels/hello.js
TuimoteDock.register({
  id: "hello",
  title: "Hello",
  order: 50,
  apiVersion: 1,
  requires: { tools: ["git"] },              // hide the tab until git resolves
  available(ctx) { return !!(ctx && ctx.cwd); },
  mount(host) {
    host.body.innerHTML = '<pre class="hello-pre" style="padding:14px"></pre>';
    this._pre = host.body.querySelector(".hello-pre");
  },
  async sync(host, ctx) {
    const { stdout, exitCode } = await host.exec("git status --short");
    this._pre.textContent = exitCode === 0 ? (stdout || "clean") : "error";
  },
});

Where the file lives decides its scope:

ScopePathTrust
user-global (every repo, just you)~/.tuimote/panels/<id>.jsruns free
repo-local (ships with one repo)<repo>/.tuimote/panels/<id>.jstrust prompt + exec allowlist

A repo-local panel must ship a sidecar <id>.panel.json declaring the command prefixes its host.exec calls may run, e.g. { "commands": ["git ", "ls "] } — the server enforces it.

Everything a page needs comes through the host facade it is handed: host.exec (argv-only, no shell — pipes, globs and $VAR are inert; compose in JS), host.fs (repo-confined listings), host.tuimote (list / spawn / message agent sessions), host.tmux (a persistent panel terminal), host.pty (a one-shot TUI like htop that dies when you look away), and host.ui (menus, toasts, sanitized markdown). Never fetch raw routes: the facade is the stability boundary, the security boundary, and what makes a panel work transparently on federated sessions — the same panel that works locally drives a repo on another machine unchanged.

Before shipping, lint it — the same checks gate loading server-side:

tuimote panel verify ~/.tuimote/panels/hello.js

It reports rule id, line, and a fix hint for each problem: structure and id checks, lifecycle-hook shapes, global-namespace collisions with the app shell, facade discipline (no raw fetch/eval), and the sidecar allowlist rules. Working built-in examples to crib from live in the tuimote repo under public/panels/.

Federation

Several machines, one dashboard: federate the tuimote servers on your network and every peer's repos and sessions appear alongside local ones, fully interactive. Federation is opt-in and off by default.

Setup on each machine — set the same pairing secret everywhere:

tuimote peers secret "one-shared-secret"
tuimote peers discovery on       # mDNS: peers on the same LAN find each other

mDNS peers exchange certificate pins automatically. For a static peer (no mDNS between the hosts), read the peer's fingerprint with tuimote peers ls on that machine, then pin it explicitly:

tuimote peers add peer-host:8787 --cert-fingerprint <sha256> [--label NAME] [--mac AA:BB:..]

Server-to-server traffic runs over TLS pinned by that fingerprint. For a peer reachable only via SSH (behind NAT, off-overlay), add --transport ssh --ssh-target <openssh-alias>: tuimote then maintains a local forward using your OpenSSH config and known_hosts. A peer with a configured MAC can be woken from the dashboard or with tuimote peers wake. Manage the rest with tuimote peers ls / toggle / remove.

Direct federation requires inbound TCP access to each server's configured port (default 8787; check with tuimote config get port). If a firewall is enabled, allow that port, for example sudo firewall-cmd --permanent --add-port=8787/tcp followed by sudo firewall-cmd --reload, or sudo ufw allow 8787/tcp. Substitute the configured port. If opening an inbound port is not possible, use the SSH transport.

Troubleshooting

Two commands cover most of it:

tuimote status        # read-only health report: server, config, security,
                      # each harness, peers, usage probes
tuimote doctor        # same checks, then offers a fix for each problem
tuimote doctor --yes  # apply every fix unattended

Service management

tuimote service status      # installed? running?
tuimote service load        # start + enable at boot
tuimote service unload      # stop
tuimote service install     # (re)write the unit file
tuimote service uninstall   # stop + remove it

The service is a launchd user agent on macOS and a systemd user unit (with lingering enabled) on Linux; it runs as your user, in your home.

Logs

tuimote server logs             # last stdout lines
tuimote server logs --err       # the error log
tuimote server logs --follow    # stream

The files live at ~/.tuimote/server.log and ~/.tuimote/server.err.log; they are rotated at 10 MiB with three numbered archives kept alongside.

Common cases