termote / website
llms.txt73 starsChanged 48 days agoMITView on GitHub
Control Claude Code, Codex, and any CLI from your phone. MIT License · Releases signed with Ed25519 (since 1.10.0) · Releases and changelog Termote is an open-source (MIT), self-hosted web app that puts the terminal sessions of your machine on your phone or another computer. One termote binary runs on Linux, macOS or Windows and serves a PWA that opens your tmux, Herdr or Zellij (Linux, macOS) sessions over the network you choose: your LAN, Tailscale or a Cloudflare Tunnel,…
- Pipes a download into a shell
- Reads credentials
- Deletes or force-pushes
- Installs packages
In this repository3 other files it gives its agents.
- CLAUDE.mdCLAUDE.md
- website/llms.txtllms.txt
- security-reviewSkill
The file
Show its rules and text
We split this llms-full.txt into the rules it gives an agent
22 rules. About 0 are common, 9 are distinctive, and 13 are very specific to this project.
≈0
Common · estimate
Many files like this have a rule of this kind.
≈9
Distinctive · estimate
This author's own choices. Worth a look.
13
Very specific
Tied to their project's names, files or tools. Probably skip.
- Common
- Distinctive
- Very specific
- Distinctive: line 61
Run the project’s tasks (npm, make, Go, Gradle, Maven, Cargo) in a tab.
- Very specific: line 197
If termote is not found right after the install, open a new terminal.
- Very specific: line 207
Run termote url --qr (or termote panel) and scan the QR code with the phone’s camera, then sign in with the same name and password.
- Distinctive: line 209
Add it to the home screen (Safari: Share → Add to Home Screen.
- Distinctive: line 339
Run Claude Code inside tmux on the computer, then connect from an SSH app on the phone (Termius, Blink Shell or another) and attach to the …
- Very specific: line 374
Start Claude Code in a session: Run claude in a terminal on the computer inside tmux, or from the phone: open a session’s terminal and type …
- Very specific: line 390
Not for Termote. It shows the Claude Code that runs in your terminal, however it is signed in: a Pro, Max or Team plan, or an API key.
- Very specific: line 514
run termote show-password in a terminal of your own, not through the agent: a command you run through it (!
- Distinctive: line 547
Put Cloudflare Access in front of it
- Very specific: line 570
Add a public hostname, for example term.example.com, with the service http://127.0.0.1:7680 (Windows: http://127.0.0.1:7690.
- Very specific: line 605
Run cloudflared on the host, pointed at http://127.0.0.1:<port>, and keep the container off the LAN.
- Distinctive: line 691
No Herdr client is attached in the container, so panes keep Herdr’s own size.
- Very specific: line 783
Use caffeinate -s (system sleep only) instead of -d (display sleep) — you don’t need the display on for remote access.
- Distinctive: line 946
Checks the port and Tailscale first: If anything fails, a server that is already running is left running, so a mistake does not cut the …
- Very specific: line 967
Every option is saved. A flag you leave out keeps its saved value, so termote start with no flags starts the server exactly as last time.
- Very specific: line 1244
If you skip this, termote start --tailscale stops before touching the running server and prints this line.
- Very specific: line 1246
Native Windows: not supported for a standard (non-administrator) user.
- Distinctive: line 1252
Start with the Tailscale name of this machine
- Very specific: line 1302
If that HTTPS port on Tailscale already serves something else, start refuses and suggests another port (--tailscale …
- Very specific: line 1321
Check it from the server while the phone is on mobile data (100.x.y.z is the phone’s Tailscale address, tailscale status lists it):
- Distinctive: line 1399
Every refusal that leaves something to check offers Open terminal.
- Distinctive: line 1448
When several choices can be ticked at once, each button ticks or unticks one option, and Next moves on with the options you ticked.
≈ “Common” is an estimate: at least 1 in 5 of the 3,592 llms.txt files we read have a rule of the same kind, not the same words. “Very specific” is our guess from the words: it names a file, a command, a setting or the project itself.
Files
<SYSTEM>This is the full developer documentation for Termote</SYSTEM>
# Your Terminal, Anywhere.
> Control Claude Code, Codex, and any CLI from your phone.

[MIT License](https://github.com/lamngockhuong/termote/blob/main/LICENSE) · Releases signed with Ed25519 (since 1.10.0) · [Releases and changelog](https://github.com/lamngockhuong/termote/releases)
Termote is an open-source (MIT), self-hosted web app that puts the terminal sessions of your machine on your phone or another computer. One `termote` binary runs on Linux, macOS or Windows and serves a PWA that opens your tmux, Herdr or Zellij (Linux, macOS) sessions over the network you choose: your LAN, Tailscale or a Cloudflare Tunnel, behind a password. From there you can answer [Claude Code or Codex as a chat](/usage/agent-chat/), [switch sessions](/usage/sessions/), read a pane’s [files and git diff](/usage/files/), and type with a [keyboard toolbar](/usage/keyboard/) made for touch. See the [security model](/usage/security/) for what is exposed and how.
## Features
[Section titled “Features”](#features)
Session Switching
A web UI for tmux, Herdr and Zellij: [switch between sessions](/usage/sessions/), tabs and panes instantly
Agent Chat
[Read and answer Claude Code or Codex as a chat](/usage/agent-chat/), with images and slash commands
Files and Changes
[Browse a pane’s files and its git diff](/usage/files/), with a Markdown preview
Mobile Optimized
[Virtual keyboard toolbar](/usage/keyboard/) with Tab, Esc, Ctrl, arrows
Gesture Support
[Swipe](/usage/gestures/) left for Ctrl+C, right for Tab, up/down to scroll
PWA Ready
[Add it to your home screen](/getting-started/#open-termote-on-your-phone) and open it like an app
Herdr Plugin
[Open the pane you are on in Termote](/usage/herdr-plugin/), or show its link as a QR code
## See It in a Minute
[Section titled “See It in a Minute”](#see-it-in-a-minute)

What the video shows
1. A phone lock screen: an agent is waiting, and you are not at your desk.
2. Install with two commands, start Termote on your network, and scan its QR code with your phone.
3. A real terminal on the phone, with Esc, Ctrl and arrow keys; swipe left for Ctrl+C, right for Tab.
4. Chat with Claude Code and Codex: send the next message, and answer its questions with one tap.
5. The files, a Markdown preview with a diagram, and the agent’s diff.
6. Upload a file from the phone; a file deleted by mistake waits in the trash.
7. Herdr workspaces or tmux sessions, with every agent’s status in one place.
8. Panes side by side: split, zoom, swap and resize them, on tmux, Herdr and Zellij.
9. Run the project’s tasks (npm, make, Go, Gradle, Maven, Cargo) in a tab; the app in English or Vietnamese.
10. A push when an agent finishes or needs you; a one-time code that pairs a view-only device.
11. Private by default: listens on 127.0.0.1, a password or paired devices, a view-only role, Tailscale HTTPS, signed releases.
12. The install command and termote.ohnice.app. Open source, MIT.
## How It Works
[Section titled “How It Works”](#how-it-works)
1. **Install it on the machine that runs your agents.** One command on Linux, macOS or Windows, no sudo: see [Getting Started](/getting-started/).
2. **Make it reachable from your phone.** `termote start --lan` opens it to your home network; `termote start --tailscale <host>` serves it over HTTPS on your [tailnet](/installation/tailscale/), away from home too.
3. **Open it on your phone.** Scan the QR code that `termote panel` or `termote url --qr` shows, sign in, and add it to the home screen. Then answer Claude Code or Codex in [Agent Chat](/usage/agent-chat/).
New to this? [Control Claude Code from your phone](/guides/claude-code-from-phone/) walks through it and compares Termote with the other ways to do it, and [Remote Control alternatives](/guides/remote-control-alternatives/) sets them side by side.
## Is It Safe?
[Section titled “Is It Safe?”](#is-it-safe)
Termote is a remote shell: whoever signs in can do what your user can on that machine. Sign-in is on by default, the server listens only on this machine until you pass `--lan`, and it refuses requests for host names that are not on its allowlist. Prefer your LAN or Tailscale to opening it to the Internet, pair other devices view-only, and read the [security model](/usage/security/) for what it protects and what it cannot.
## Quick Install
[Section titled “Quick Install”](#quick-install)
```bash
curl -fsSL https://termote.ohnice.app/install.sh | sh # Linux, macOS
termote start
```
```powershell
irm https://termote.ohnice.app/install.ps1 | iex # Windows
termote start
```
## Deployment Modes
[Section titled “Deployment Modes”](#deployment-modes)
| Mode | Command | Use Case | Platform |
| --------- | ---------------------- | ------------------------------------ | --------------------- |
| Native | `termote start` | Host tool access (Claude Code) | macOS, Linux, Windows |
| Container | `termote container up` | Isolated environment (Podman/Docker) | macOS, Linux, Windows |
# About Termote
> Who makes Termote, how it is licensed and released, how to report a bug or a vulnerability, and what the site counts about its visitors.
Termote is an open-source, self-hosted web app that puts the terminal sessions of your machine, and the Claude Code or Codex running in them, on your phone or another computer. The name is **Term**inal + Re**mote**.
## Maintainer
[Section titled “Maintainer”](#maintainer)
Termote is made by **Lam Ngoc Khuong**, a software engineer based in Vietnam who has spent over a decade building systems and leading engineering teams, and who now focuses on building small, useful tools.
* Website: [khuong.dev](https://khuong.dev/en/about/)
* GitHub: [@lamngockhuong](https://github.com/lamngockhuong)
* LinkedIn: [lamngockhuong](https://www.linkedin.com/in/lamngockhuong)
## Why Termote
[Section titled “Why Termote”](#why-termote)
In the maintainer’s words:
> I have used VS Code for years, but in the age of AI agents it has become too heavy for what I still do in it: look at files, read Markdown, and see what changed. I also wanted to reach my machine from my phone, to answer an agent’s questions quickly instead of sitting at the computer all day. Those are the main reasons I made Termote.
## The Project
[Section titled “The Project”](#the-project)
* **Open source:** the code is on GitHub at [lamngockhuong/termote](https://github.com/lamngockhuong/termote), under the [MIT License](https://github.com/lamngockhuong/termote/blob/main/LICENSE).
* **History:** started in March 2026; 1.0 was released on 2026-09-28.
* **Releases:** several a week. Each one is listed on [GitHub Releases](https://github.com/lamngockhuong/termote/releases) and in the [changelog](https://github.com/lamngockhuong/termote/blob/main/CHANGELOG.md), and `termote update` installs the newest.
* **Signed:** from 1.10.0, every release’s checksums are signed with Ed25519; the installer and `termote update` check that signature before installing.
## Reporting a Bug or a Vulnerability
[Section titled “Reporting a Bug or a Vulnerability”](#reporting-a-bug-or-a-vulnerability)
* A bug or an idea: open a [GitHub issue](https://github.com/lamngockhuong/termote/issues).
* A vulnerability: report it privately, never in a public issue, as the [security policy](https://github.com/lamngockhuong/termote/blob/main/SECURITY.md) explains. The [security model](/usage/security/) describes what Termote protects and what it cannot.
## Contributing and Support
[Section titled “Contributing and Support”](#contributing-and-support)
Contributions are welcome: see the [contributing guide](https://github.com/lamngockhuong/termote/blob/main/CONTRIBUTING.md). To support the work, use the links at the bottom of every page (GitHub Sponsors, Buy Me a Coffee, MoMo).
## Analytics
[Section titled “Analytics”](#analytics)
This site counts page views and a few clicks with [GoatCounter](https://www.goatcounter.com/): no cookies, nothing personal, and never on the [demo](/demo/).
# Getting Started
> Install Termote on Linux, macOS or Windows with two commands, start the server, and open your terminal sessions and Claude Code from your phone.
Termote puts the terminal sessions of your machine, and the Claude Code or Codex running in them, on your phone or another computer. It is one binary: the PWA, the terminal stream, the API and the login all live in it. You install it with one command, then `termote start` registers it as a service of your user and starts it.
## Prerequisites
[Section titled “Prerequisites”](#prerequisites)
[tmux](https://github.com/tmux/tmux) on Linux or macOS (`sudo apt install tmux`, `brew install tmux`), or [psmux](https://github.com/psmux/psmux) on Windows (`winget install psmux`); Herdr, Zellij and herdr-core work too ([backends](/installation/native/#prerequisites)), and [container mode](/installation/docker/) needs Podman or Docker instead. No sudo or administrator rights are needed: everything goes into your home directory.
## Install
[Section titled “Install”](#install)
* Linux / macOS
```bash
curl -fsSL https://termote.ohnice.app/install.sh | sh
termote start
```
* Windows (PowerShell)
```powershell
irm https://termote.ohnice.app/install.ps1 | iex
termote start
```
1. The installer picks the newest stable release (1.0 or later), downloads the archive for your platform, checks its `.sha256` (always; there is no way to skip it) and puts the `termote` command on your PATH. It never starts anything.
2. `termote start` detects the terminal backend, creates the admin password and prints it once (in a terminal only), registers the server with your OS’s service manager and starts it. See [what `termote start` does](/installation/native/#what-termote-start-does).
3. Open the URL it prints, by default <http://127.0.0.1:7680> (Windows: <http://127.0.0.1:7690>), and log in as `admin` (or the name set with `--user`) with that password. Forgot it? `termote show-password`.
Tip
If `termote` is not found right after the install, open a new terminal. On Linux/macOS the installer prints the `export PATH=...` line to add when `~/.local/bin` is not on your PATH yet.
## Open Termote on Your Phone
[Section titled “Open Termote on Your Phone”](#open-termote-on-your-phone)

1. Make it reachable from the phone: `termote start --lan` on your home network, or `termote start --tailscale <host>` for HTTPS through [Tailscale](/installation/tailscale/), away from home too. The option is saved for the next start.
2. Run `termote url --qr` (or `termote panel`) and scan the QR code with the phone’s camera, then sign in with the same name and password. To skip the password on the phone, [pair it](/usage/devices/) with a one-time code.
3. Add it to the home screen (Safari: **Share → Add to Home Screen**; Chrome: **Add to Home screen** or **Install app**) to open it like an app. On an iPhone or iPad, agent [notifications](/usage/settings/#notify-when-an-agent-needs-me) work only from the Home Screen app.
4. Open a pane running Claude Code or Codex, and switch to [Agent Chat](/usage/agent-chat/) to read and answer it.
## Pin a version, or repair an install
[Section titled “Pin a version, or repair an install”](#pin-a-version-or-repair-an-install)
Set `TERMOTE_VERSION` to install one exact release (`X.Y.Z`, or `X.Y.Z-rc.N` for a pre-release):
* Linux / macOS
```bash
curl -fsSL https://termote.ohnice.app/install.sh | TERMOTE_VERSION=1.0.0 sh
```
* Windows (PowerShell)
```powershell
$env:TERMOTE_VERSION='1.0.0'; irm https://termote.ohnice.app/install.ps1 | iex
```
Without `TERMOTE_VERSION`, the installer leaves an existing install alone and tells you to run `termote update`. With it, the installer puts that version beside the existing one and switches to it, which is also how you rescue an install that no longer runs. Your config is kept either way.
`GH_TOKEN` or `GITHUB_TOKEN`, when set, only raise the GitHub API rate limit the installer uses to find the newest release.
## Pre-releases
[Section titled “Pre-releases”](#pre-releases)
The installer, like `termote update`, only picks a pre-release (`X.Y.Z-rc.N`) when you name it with `TERMOTE_VERSION` (or `termote update --version X.Y.Z-rc.N`). Without it, you get the newest **published** stable release (1.0 or later): a release still waiting to be published already has its tag, but none of its files can be downloaded yet. The same goes for a pre-release you name: until it is published, the install fails with `still publishing`.
This site serves the installer of the last stable release. To run a pre-release’s own installer, take it from its tag and still name the version:
* Linux / macOS
```bash
curl -fsSL https://raw.githubusercontent.com/lamngockhuong/termote/v1.13.0-rc.1/scripts/install.sh | TERMOTE_VERSION=1.13.0-rc.1 sh
```
* Windows (PowerShell)
```powershell
$env:TERMOTE_VERSION='1.13.0-rc.1'; irm https://raw.githubusercontent.com/lamngockhuong/termote/v1.13.0-rc.1/scripts/install.ps1 | iex
```
To go back to a stable release, run `termote update`. Until the final `X.Y.Z` is out, this is a downgrade (a pre-release counts as newer than the release before it), which `update` makes with a warning.
Coming from Termote 0.x
There is no upgrade from 0.x: 1.0 does not read the config or anything else 0.x left behind. Uninstall 0.x first, following the archived [0.x native](/0.x/installation/native/#uninstall) or [0.x container](/0.x/installation/docker/#uninstall) page, then install 1.0 as above.
## Commands
[Section titled “Commands”](#commands)
Flags use the same syntax on every OS, PowerShell included (`termote start --lan`).
| Command | What it does |
| ------------------------- | -------------------------------------------------------------------------------- |
| `termote start [options]` | Save the options, register the service and start it |
| `termote stop` | Stop the server (it starts again at the next login) |
| `termote restart` | Stop and start with the saved options |
| `termote status` | Show what the running server reports (alias: `health`) |
| `termote logs [service]` | View logs: `server`, `all` (default), `follow`, `clean` |
| `termote show-password` | Show the saved username and password |
| `termote pair [options]` | Make a one-time code that signs a new device in (`--role view\|full`, `--name`) |
| `termote devices` | List the paired devices; `devices revoke <id>` signs one out |
| `termote update` | Update to the latest release, rolling back if the new one does not come up |
| `termote uninstall` | Remove the service, the command and the install (`--purge`: config and logs too) |
| `termote container <cmd>` | Run the server in a container: `up`, `down`, `logs [-f]`, `status` |
| `termote link` / `unlink` | Create or remove the `termote` command in `~/.local/bin` |
| `termote version` | Show the version |
| `termote serve` | Run the server in the foreground (what the service runs) |
| `termote` | Interactive menu |
`termote help` prints the same list with every option. The old `install` command is gone: typing it only points you to `termote start` and `termote container up`.
## File Locations
[Section titled “File Locations”](#file-locations)
| What | Linux / macOS | Windows |
| --------------- | ------------------------------------------------------------ | ---------------------------------------------------------- |
| Install root | `~/.local/share/termote` (`$XDG_DATA_HOME/termote`) | `%LOCALAPPDATA%\termote` |
| Versions | `versions/<v>/bin/termote` | `versions\<v>\bin\termote.exe` |
| Current version | `current` symlink to `versions/<v>` | `current.txt` plus the `bin\termote.cmd` launcher |
| Command | `~/.local/bin/termote` | `%LOCALAPPDATA%\termote\bin`, added to your user PATH |
| Config | `~/.config/termote/config` and `secret` (`$XDG_CONFIG_HOME`) | `%APPDATA%\termote\config.json` (password DPAPI-encrypted) |
| Logs, PID file | `~/.local/state/termote/` (`$XDG_STATE_HOME`) | `%LOCALAPPDATA%\termote\state` |
## Next Steps
[Section titled “Next Steps”](#next-steps)
* [Control Claude Code from Your Phone](/guides/claude-code-from-phone/) - The whole setup, step by step
* [Agent Chat](/usage/agent-chat/) - Read and answer Claude Code and Codex from your phone
* [Pair a Device](/usage/devices/) - Sign a phone in with a one-time code instead of the password
* [Tailscale HTTPS](/installation/tailscale/) - Secure remote access, away from home too
* [Security Model](/usage/security/) - What Termote protects, what it cannot, and what to do when a device is lost
* [Native](/installation/native/) - Start options, the service, update and uninstall
* [Container](/installation/docker/) - Run the server in Podman or Docker
# Control Claude Code from Your Phone
> Three ways to watch and answer Claude Code on your machine from a phone: Anthropic's Remote Control, SSH with tmux, and self-hosted Termote, step by step.
Claude Code runs on your computer, in a terminal, and every now and then it stops: to ask for a permission, to ask a question, or because it finished. If you are not at your desk, it waits. This guide compares three ways to reach that session from a phone, then sets up the self-hosted one, Termote, step by step.
Note
The comparison was checked against each tool’s own documentation in November 2026. Tools change: follow the links to confirm a detail that matters to you.
## Three Ways to Reach Claude Code from a Phone
[Section titled “Three Ways to Reach Claude Code from a Phone”](#three-ways-to-reach-claude-code-from-a-phone)
### Anthropic’s Remote Control
[Section titled “Anthropic’s Remote Control”](#anthropics-remote-control)
[Remote Control](https://code.claude.com/docs/en/remote-control) connects the Claude app or claude.ai/code to a Claude Code session on your machine. `claude remote-control` waits for connections and can start new sessions from the phone; it shows a QR code to scan.
* **Good:** nothing to install on the phone besides the Claude app, no port opened on your machine (it makes outbound HTTPS requests only), images and files from the phone, and the session reconnects after the laptop sleeps.
* **Limits:** it needs a Pro, Max, Ultra, Team or Enterprise plan; API keys are not supported, and on Team and Enterprise an Owner has to turn it on. While connected, the session’s transcript is stored on Anthropic’s servers. It shows Claude Code only, not your other terminals or agents.
### SSH, tmux and a Phone Terminal
[Section titled “SSH, tmux and a Phone Terminal”](#ssh-tmux-and-a-phone-terminal)
Run Claude Code inside [tmux](https://github.com/tmux/tmux) on the computer, then connect from an SSH app on the phone (Termius, Blink Shell or another) and attach to the tmux session.
* **Good:** works with any plan or an API key, and with every terminal program, not only Claude Code; nothing leaves your network but the SSH connection you set up.
* **Limits:** you read Claude Code as a terminal on a phone screen and type its keys on a phone keyboard; a phone usually suspends the SSH app in the background, so the connection drops and you attach again (Mosh helps); nothing tells you when Claude Code is waiting for you.
### Termote
[Section titled “Termote”](#termote)
[Termote](/) is a self-hosted web app (MIT License) that serves your tmux, Herdr or Zellij sessions to a browser, with Claude Code and Codex shown as a chat.
* **Good:** works with however Claude Code is signed in (a plan or an API key), since Termote only reads and types into its terminal; a [Chat view](/usage/agent-chat/) with buttons for Claude Code’s dialogs; a push [notification](/usage/settings/#notify-when-an-agent-needs-me) when an agent finishes or needs you; the [files and git diff](/usage/changes/) of the session; every terminal of the machine, not only Claude Code. Nothing goes through a third-party server: the phone talks to your machine over the network you choose.
* **Limits:** you install it on the computer and choose how the phone reaches it (your LAN, Tailscale or a Cloudflare Tunnel); the computer must stay on, as with every option here. It is a remote shell: anyone who signs in can do what your user can, so read the [security model](/usage/security/).
## Set Up Termote Step by Step
[Section titled “Set Up Termote Step by Step”](#set-up-termote-step-by-step)
1. **Install Termote on the computer that runs Claude Code** (Linux, macOS or Windows, no sudo). You also need tmux on Linux and macOS, or psmux on Windows: see [Getting Started](/getting-started/).
```bash
curl -fsSL https://termote.ohnice.app/install.sh | sh
```
2. **Start it on a network your phone can reach.** At home, `--lan` opens it to your LAN. Away from home, [Tailscale](/installation/tailscale/) serves it over HTTPS on your tailnet, with no port opened to the Internet.
```bash
termote start --lan # same Wi-Fi
termote start --tailscale myhost.tailnet-name.ts.net # anywhere, over Tailscale HTTPS
```
3. **Open it on the phone.** `termote url --qr` prints a QR code of the address; scan it, then sign in with the name and password `termote start` printed (`termote show-password` shows them again). To skip the password on the phone, [pair it](/usage/devices/) with a one-time code.
4. **Add it to the home screen.** Safari: **Share → Add to Home Screen**; Chrome: **Add to Home screen** or **Install app**. It then opens like an app, and on an iPhone or iPad this is what lets notifications arrive.
5. **Start Claude Code in a session.** Run `claude` in a terminal on the computer inside tmux, or from the phone: open a session’s terminal and type it. With Herdr, an empty pane’s Chat view has a **Claude Code** button that starts it.
6. **Answer it in Chat.** Switch the pane to **Chat**: Claude Code’s messages show as a conversation, and its permission dialogs and questions as buttons. Type the next message at the bottom.
7. **Turn on notifications.** Settings → Sessions → **Notify when an agent needs me**: the phone gets a push when an agent finishes or waits on a dialog, even with the app closed.

## Questions
[Section titled “Questions”](#questions)
### Do I need a Claude Pro or Max plan?
[Section titled “Do I need a Claude Pro or Max plan?”](#do-i-need-a-claude-pro-or-max-plan)
Not for Termote. It shows the Claude Code that runs in your terminal, however it is signed in: a Pro, Max or Team plan, or an API key. Anthropic’s Remote Control is the one that needs a plan.
### Can I use an API key?
[Section titled “Can I use an API key?”](#can-i-use-an-api-key)
Yes. Termote never talks to Anthropic; Claude Code does, with whatever credentials it already has.
### Can I start a new Claude Code session from my phone?
[Section titled “Can I start a new Claude Code session from my phone?”](#can-i-start-a-new-claude-code-session-from-my-phone)
Yes. Add a session from the sessions list (**New session**) and type `claude` in its terminal, or, with Herdr, tap **Claude Code** in an empty pane’s Chat view. See [Sessions](/usage/sessions/) and [Agent Chat](/usage/agent-chat/#starting-an-agent-herdr).
### Does it stop when the phone locks?
[Section titled “Does it stop when the phone locks?”](#does-it-stop-when-the-phone-locks)
No. Claude Code keeps running in tmux, Herdr or Zellij on the computer, whatever the phone does. When you open Termote again, it reconnects and shows where the session is now; a notification tells you if the agent needed you meanwhile.
### Is it safe to open to the network?
[Section titled “Is it safe to open to the network?”](#is-it-safe-to-open-to-the-network)
Sign-in is on by default, the server listens only on the computer until you pass `--lan`, and it refuses host names that are not on its allowlist. Prefer your LAN or Tailscale to opening it to the Internet, and if you use a Cloudflare Tunnel, put [Cloudflare Access](/installation/cloudflare/) in front of it. The [security model](/usage/security/) lists what Termote protects and what it cannot.
### Does it work with Codex too?
[Section titled “Does it work with Codex too?”](#does-it-work-with-codex-too)
Yes: the Chat view shows Codex like Claude Code, and any other agent or CLI works in the terminal view. See [Agent Chat](/usage/agent-chat/#codex).
## Next Steps
[Section titled “Next Steps”](#next-steps)
* [Agent Chat](/usage/agent-chat/) - What the Chat view shows and how to answer dialogs
* [Agent Overview](/usage/overview/) - Every agent of every session on one screen
* [Pair a Device](/usage/devices/) - Sign a phone in with a one-time code
* [Remote Control alternatives](/guides/remote-control-alternatives/) - Termote and other options side by side
# Remote Control Alternatives
> Termote, Anthropic's Remote Control and an SSH app with tmux side by side: where each runs, the Claude plan it needs, new sessions, agents, files and license.
Anthropic’s Remote Control is one way to reach Claude Code from a phone, and not the only one. This page sets it beside two self-hosted options: an SSH app with tmux, and Termote, which this site documents.
Note
Checked against each project’s own documentation in November 2026. A cell says only what that documentation says. Follow the source links before relying on a detail.
## Side by Side
[Section titled “Side by Side”](#side-by-side)
| | Termote | Remote Control | SSH app + tmux |
| -------------------------- | -------------------------------------------------------- | ---------------------------------------------------- | ---------------------- |
| Runs on | Your machine, reached directly | Your machine, relayed through Anthropic’s API | Your machine, over SSH |
| Claude plan needed | None (plan or API key) | Pro, Max, Ultra, Team or Enterprise; no API keys | None |
| New session from the phone | Yes | Yes, in server mode | Yes (`tmux new`) |
| Agents | Claude Code and Codex as a chat; any CLI in the terminal | Claude Code only | Any CLI, as a terminal |
| Phone keys | Toolbar with Tab, Esc, Ctrl, arrows; gestures | Chat app, no terminal | Depends on the SSH app |
| Files and git diff | File browser, previews, diff and staging | Send images and files to the session | Through shell commands |
| Notifications | Web Push when an agent finishes or needs you | Push when a task finishes or Claude needs a decision | None |
| License | MIT | Part of Claude Code | tmux: ISC; apps vary |
| Machine must stay on | Yes | Yes | Yes |
Sources: [Termote docs](/getting-started/), [Remote Control](https://code.claude.com/docs/en/remote-control), [tmux](https://github.com/tmux/tmux).
## When Each One Fits
[Section titled “When Each One Fits”](#when-each-one-fits)
* **Remote Control** fits if you already pay for a Claude plan, want nothing to install on the phone besides the Claude app, and only need Claude Code. Nothing listens on your machine; in exchange the session’s transcript is stored on Anthropic’s servers while connected, and API-key setups are left out.
* **An SSH app with tmux** fits if a terminal on the phone is enough and you already reach the machine over SSH. It needs nothing new on the computer, but nothing tells you when the agent waits.
* **Termote** fits if you want Claude Code and Codex as a chat, the session’s files and git diff, and every terminal of the machine in one app, on any Claude plan or an API key, with tmux, Herdr or Zellij.
## Where Termote Is Weaker
[Section titled “Where Termote Is Weaker”](#where-termote-is-weaker)
* You install and run it yourself, and choose how the phone reaches it: your LAN, Tailscale, or a Cloudflare Tunnel behind Access. Remote Control needs no network setup at all.
* The computer has to stay on and awake. That is true of every option here, Remote Control included.
* It is a remote shell: whoever signs in with full access can do what your user can. Read the [security model](/usage/security/) before you open it beyond your own machine.
* The Chat view covers Claude Code and Codex. Other agents, such as GitHub Copilot CLI, work in the terminal view only.
## Next Steps
[Section titled “Next Steps”](#next-steps)
* [Control Claude Code from your phone](/guides/claude-code-from-phone/) - Set up Termote step by step
* [Getting Started](/getting-started/) - Install with two commands
* [Security Model](/usage/security/) - What Termote protects, and what it cannot
# Install with an agent
> Let Claude Code, Codex or another coding agent install and start Termote, and keep the password out of its transcript
A coding agent running on the machine that will run Termote can install and start it for you. Paste this prompt into Claude Code, Codex or another agent:
```text
Install and start Termote on this machine. Follow https://termote.ohnice.app/agents.md.
Ask me how I want to reach it (only this machine, my phone on the same Wi-Fi, Tailscale,
or a Cloudflare Tunnel I run) before you start it. Do not show me the password; tell me
to run termote show-password.
```
## What the agent does
[Section titled “What the agent does”](#what-the-agent-does)
1. Asks where you will open Termote from, and whether it runs natively or in a container.
2. Checks whether Termote is already installed (`termote version`, `termote status --json`), and installs it with the [online installer](/installation/native/) or Homebrew when it is not. It never uses `sudo`.
3. Starts it with the options that match your answer, such as `termote start --lan`.
4. Checks that the server answers with `termote status --json`, then gives you the link.
The agent follows [`/agents.md`](/agents.md), a guide written for agents. Once Termote is installed, `termote help agent` prints the same guide for the installed version.
## The password stays with you
[Section titled “The password stays with you”](#the-password-stays-with-you)
An agent’s output goes into its transcript, which may be sent to the model provider. So the agent does not read the password: when `termote start` or `termote container up` runs outside a terminal, it prints the username and no password. See it yourself:
* run `termote show-password` in a terminal of your own, not through the agent: a command you run through it (`!` in Claude Code) goes into its transcript too, and a view-only device watching the terminal sees it;
* or run `termote panel`, which shows the links and a QR code to open on your phone.
Note
To sign a phone in without typing the password, run `termote pair --role full` yourself and enter the code on the phone ([Pair a Device](/usage/devices/)). The code is a secret too, so do not ask the agent to make it.
## Settings the agent proposes
[Section titled “Settings the agent proposes”](#settings-the-agent-proposes)
Termote’s settings live in each browser, not on the server. Ask the agent for a change, such as “use Vim keys in Termote’s editor”: it runs `termote settings propose`, and every browser open on Termote shows a bar to review the change, setting by setting, and apply or dismiss it. Nothing changes until you apply it, and then only in that browser. Notifications and the count on the app icon ask for a permission, so you turn them on in Settings yourself. The container install has no way for the agent to propose: move settings between browsers with Settings → Export & import ([Settings](/usage/settings/#export-and-import)).
## For agents and tools
[Section titled “For agents and tools”](#for-agents-and-tools)
* [`/agents.md`](/agents.md): how to install, start, check and hand over Termote.
* [`/llms.txt`](/llms.txt): an index of these docs; [`/llms-full.txt`](/llms-full.txt) holds every page.
## See also
[Section titled “See also”](#see-also)
* [Native installation](/installation/native/) and [Container](/installation/docker/)
* [Tailscale HTTPS](/installation/tailscale/) and [Cloudflare Tunnel](/installation/cloudflare/)
# Cloudflare Tunnel
> Reach Termote from anywhere through a Cloudflare Tunnel you run, behind Cloudflare Access, with the hostname allowed and sign-in limits per client.
A Cloudflare Tunnel gives Termote an HTTPS address on your own domain without opening a port. You run the tunnel (`cloudflared`); Termote only needs to know its hostname. Termote never starts `cloudflared` and never sees the tunnel token.
Put Cloudflare Access in front of it
A tunnel puts the sign-in page on the Internet. Without Cloudflare Access, the Termote password is all that stands between a stranger and a shell on your machine. Termote checks for Access when it starts and in `termote status`, and warns loudly when it is missing. The [security model](/usage/security/) lists what a signed-in client can do.
## Prerequisites
[Section titled “Prerequisites”](#prerequisites)
* A domain on Cloudflare (any plan, Free included)
* `cloudflared` installed on the machine that runs Termote ([downloads](https://developers.cloudflare.com/cloudflare-one/networks/connectors/cloudflare-tunnel/downloads/))
## Setup
[Section titled “Setup”](#setup)
1. In the Cloudflare dashboard, open **Zero Trust → Networks → Tunnels** and create a tunnel (type **Cloudflared**).
2. Install it as a service with the command the dashboard shows. It holds the token, so it runs as root on Linux and macOS and as an administrator on Windows:
```bash
sudo cloudflared service install <token>
```
3. Add a **public hostname**, for example `term.example.com`, with the service `http://127.0.0.1:7680` (Windows: `http://127.0.0.1:7690`; the container: its `--port`).
4. Turn on Cloudflare Access for that hostname: **Zero Trust → Access → Applications → Add an application → Self-hosted**, with the same hostname and a policy that lets in only you (for example, your email).
5. Tell Termote the hostname
* Native
```bash
termote start --cloudflare term.example.com
```
* Container
```bash
termote container up --cloudflare term.example.com
```
6. Open <https://term.example.com>: Access asks who you are, then Termote asks for its password.
## What `--cloudflare` does
[Section titled “What --cloudflare does”](#what---cloudflare-does)
* Adds the hostname to the allowed `Host` names (like `--allow-host`), so requests through the tunnel are not refused.
* Shows `https://term.example.com` in `termote url`, `termote panel` (and its QR code) and `termote status` (`cloudflare` and `urls.cloudflare` in `status --json`).
* Counts failed sign-ins per client, by the `CF-Connecting-IP` header the tunnel sets. Five wrong passwords lock out only the client that typed them. Every attempt through a proxy still shares one limit of 30 a minute, so a program on your machine that sets the header itself cannot guess faster that way; many addresses guessing together can fill that limit and hold everyone signing in through a proxy off for a minute. Natively, sign-ins on the machine itself without a proxy have a limit of 30 of their own. With Access in front, strangers never get that far.
* Checks from outside, at `start`, `container up`, `status` and when `panel` opens, whether Access answers for the hostname (`cloudflareAccess` in `status --json`: `on`, `off` or `unknown`). `unknown` usually means `cloudflared` is not running.
The option is saved; `--no-cloudflare` forgets it. It is refused together with `--no-auth`.
## Container
[Section titled “Container”](#container)
Run `cloudflared` on the host, pointed at `http://127.0.0.1:<port>`, and keep the container off the LAN. `container up` refuses `--cloudflare` together with `--lan`: inside the container a LAN machine arrives the same way as the tunnel and could set the client header itself.
## Native: the user `cloudflared` runs as
[Section titled “Native: the user cloudflared runs as”](#native-the-user-cloudflared-runs-as)
On Linux and Windows the server refuses connections from other local users. `cloudflared` installed as a service runs as root (SYSTEM on Windows), which is allowed. If you run it as another user, allow that user: `termote start --allow-local-user <name>`.
## Other tunnels
[Section titled “Other tunnels”](#other-tunnels)
| Tool | What to do |
| ------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| ngrok | `termote start --proxy-client-header X-Forwarded-For --allow-host <your ngrok domain>`; Termote takes the last address of the list, the one ngrok adds. Turn on ngrok’s own sign-in (OAuth) |
| VS Code Dev Tunnels | The client header does not help: the service puts its own internal address there. Keep the tunnel private (signed in with your GitHub or Microsoft account) |
| A reverse proxy on this machine | `--proxy-client-header` with the header it sets to the client’s address (for example `X-Real-IP`) |
`--proxy-client-header` overrides the `CF-Connecting-IP` default of `--cloudflare`; `--no-proxy-client-header` stops reading a header.
## Notes
[Section titled “Notes”](#notes)
* **Pseudo IPv4.** With **Network → Pseudo IPv4** set to *Overwrite headers*, `CF-Connecting-IP` holds a made-up IPv4 address for IPv6 visitors. Leave it *Off* or on *Add header*.
* **Uploads.** Cloudflare’s Free and Pro plans accept request bodies up to 100 MB, so a larger file upload through the tunnel fails.
* **WebSockets** need nothing: Termote pings the terminal stream every 15 seconds, well inside Cloudflare’s idle timeout.
# Container Installation
> Run the Termote server in Podman or Docker with termote container up: options, the terminal backend, updating, troubleshooting and uninstalling.
`termote container up` runs the server in a container instead of on the host: termote (PWA + terminal stream + API) and a terminal backend (tmux, Herdr or Zellij) in one image, with a workspace directory mounted from the host. Use it when the terminals do not need your host tools (like `claude` or `git` as installed on the host).
## Prerequisites
[Section titled “Prerequisites”](#prerequisites)
* Podman (preferred when both are installed) or Docker, including Docker Desktop or Podman Desktop on macOS and Windows.
* The `termote` command, installed as in [Getting Started](/getting-started/). You do not need to run `termote start`.
## Quick Start
[Section titled “Quick Start”](#quick-start)
1. Start the container
```bash
termote container up
```
2. Open <http://127.0.0.1:7680> and log in as `admin` (or the name set with `--user`) with the password it prints (only in a terminal; otherwise run `termote show-password`).
3. (Optional) Expose it to the LAN; the option is saved like any other
```bash
termote container up --lan
```
## How It Runs
[Section titled “How It Runs”](#how-it-runs)
* The image is `ghcr.io/lamngockhuong/termote:<version>`, where `<version>` is the version of your `termote` binary, so the container always matches the command that started it. From 1.10.0 on it is pulled by the digest the release’s signed checksums name, never by the tag. `container up` pulls it, replaces any existing `termote` container and waits until the server answers.
* It runs as your user and group, with `--restart unless-stopped`, and mounts the workspace directory (default `~/termote-workspace`, created if missing) at `/workspace`. It warns when the workspace contains sensitive directories such as `.ssh` or `.aws`.
* The password and settings reach the container through the environment of the `docker`/`podman` process (`-e NAME`, no value), so they never appear on a command line or in a file on disk (`docker inspect` still shows them, as with any environment variable).
* The container settings are saved apart from the native `termote start` options, but the username and password are shared: a new one (`--user`, `--fresh`) applies to both, and the native server takes it at its next `termote restart`.
* The container’s default port is 7680. If the native server already uses that port, `container up` refuses and suggests `--port`.
* From a git checkout, or with `--build`, it builds `termote:local` from the Dockerfile instead of pulling.
## Terminal Backend
[Section titled “Terminal Backend”](#terminal-backend)
The image carries tmux, [Herdr](https://herdr.dev) and [Zellij](https://zellij.dev), so there is nothing to install or detect. The first `container up` without `--mux` asks which one to use; without a terminal to ask in, it uses tmux and prints how to switch. The choice is saved with the container settings, apart from the native `--mux`:
```bash
termote container up --mux herdr # Herdr workspaces and agent status badges
termote container up --mux zellij # Zellij sessions, on any host
termote container up # keeps the saved backend
termote container up --mux tmux # back to tmux
```
Herdr in the container
* Herdr runs inside the container, so it sees only the container’s terminals, never the Herdr server or the agents running on your host.
* No Herdr client is attached in the container, so panes keep Herdr’s own size; the PWA fits the terminal to it instead of resizing the pane.
* `--mux herdr --no-auth` is refused unless `--allow-herdr-no-auth` is also given, as with `termote start`.
Zellij in the container
* The image ships Zellij 0.45.1 (the static build without Zellij’s web server, pinned by version and sha256), so `--mux zellij` works on any host, Windows included.
* The container keeps its own Zellij config (no setup wizard, tips or release notes) and starts the session `main`; Zellij there sees only the container’s sessions.
After a reboot
Docker’s daemon restarts the container after a reboot. Rootless Podman has no daemon, so the container stays stopped; to keep it running across reboots, run it as a [Quadlet](https://docs.podman.io/en/latest/markdown/podman-systemd.unit.5.html) systemd unit.
## Options
[Section titled “Options”](#options)
Like `termote start`, every option is saved, a flag you leave out keeps its saved value, and a boolean is turned off with `=false`.
| Option | Description |
| ------------------------------ | ------------------------------------------------------------------------------------------------------- |
| `--port <port>` | Port on the host (default 7680) |
| `--lan[=false]` | Expose to the LAN (default: this machine only) |
| `--tailscale <host[:port]>` | Publish over [Tailscale HTTPS](/installation/tailscale/) (default 443) |
| `--no-tailscale` | Stop publishing over Tailscale |
| `--cloudflare <host>` | Hostname of a [Cloudflare Tunnel](/installation/cloudflare/) you run (not with `--no-auth` or `--lan`) |
| `--no-cloudflare` | Forget the Cloudflare hostname |
| `--proxy-client-header <name>` | Header a proxy sets to the client’s address (default with `--cloudflare`: `CF-Connecting-IP`) |
| `--no-proxy-client-header` | Stop reading it |
| `--no-auth[=false]` | Disable the login |
| `--mux <tmux\|herdr\|zellij>` | Terminal backend (asked the first time, tmux without a terminal) |
| `--allow-herdr-no-auth` | Required together with `--mux herdr --no-auth` |
| `--allow-host <name>` | Allow another `Host` header name (repeatable, no wildcard) |
| `--remove-host <name>` | Remove an allowed name (repeatable) |
| `--allow-path <dir>` | Mount a host folder as a [Place](/usage/files/#places) in Files (repeatable; `--remove-path` drops one) |
| `--workspace <dir>` | Directory mounted at `/workspace` (default `~/termote-workspace`) |
| `--user <name>` | Login username (default `admin`; shared with the native server) |
| `--fresh` | Set a new password (shared with the native server) |
| `--build` | Build the image from a checkout instead of pulling it |
Windows and --lan
On Windows the container port is published on localhost only, and `--lan` adds a `netsh` port forwarding rule and a firewall rule, which asks for administrator rights. `container down` removes them again.
Give Termote a host name of its own
Browsers send a site’s cookies to every port of its host name, so another web service on the same name (another port on the same server, another app on the same domain) receives Termote’s session cookie whenever you open it, and with it a signed-in terminal. On a server that runs anything else, reach Termote under a name only it answers on: a Tailscale name, or a name of its own such as `termote.example.com`. Log out from the **More** menu on a device you no longer use.
## Managing the Container
[Section titled “Managing the Container”](#managing-the-container)
```bash
termote container status # is it running, and what the server reports
termote container logs # recent log lines
termote container logs -f # follow the logs (Ctrl+C to stop)
termote container down # remove the container (and its Tailscale mapping)
```
`termote stop`, `restart` and `uninstall` only act on the native server; the container has its own commands.
## Updating
[Section titled “Updating”](#updating)
`termote update` updates the command, and the image tag follows it. Run `container up` again to move the container to the image of the new version, with the saved settings:
```bash
termote update
termote container up
```
## Prevent macOS Sleep
[Section titled “Prevent macOS Sleep”](#prevent-macos-sleep)
macOS automatically sleeps after inactivity, disconnecting remote terminal sessions — even with containers running. Use `caffeinate` to keep the machine awake:
```bash
# Prevent system sleep (runs until you press Ctrl+C)
caffeinate -s
# Or run in background
caffeinate -s &
# Stop background caffeinate
pkill caffeinate
```
Tip
Use `caffeinate -s` (system sleep only) instead of `-d` (display sleep) — you don’t need the display on for remote access.
## Troubleshooting
[Section titled “Troubleshooting”](#troubleshooting)
### Debug inside container
[Section titled “Debug inside container”](#debug-inside-container)
```bash
docker exec -it termote /bin/sh
# Inside container:
ps aux # Check processes (termote, tmux)
```
Replace `docker` with `podman` if that is your runtime.
### Common Issues
[Section titled “Common Issues”](#common-issues)
| Issue | Solution |
| ---------------------------- | ------------------------------------------------------------------------------------------------- |
| Neither podman nor docker | Install one of them, then `termote container up` again |
| Port in use | Often the native server: `termote container up --port <port>`, or `termote stop` first |
| Image cannot be pulled | A pre-release has no image until it is published; install a stable release |
| Server does not answer | `termote container logs` shows why |
| Gone after a reboot (Podman) | Rootless Podman does not restart it: `termote container up` again, or use a Quadlet unit |
| Host rejected (403) | Add the name the error suggests: `termote container up --allow-host <name>` |
| Stale PWA cache | More (⋯) menu → “Clear cache & reload” to refresh (it also signs out and drops unsaved file text) |
### API Test
[Section titled “API Test”](#api-test)
```bash
curl -u admin:<password> http://127.0.0.1:7680/api/mux/health # or the name set with --user
```
## Uninstall
[Section titled “Uninstall”](#uninstall)
```bash
termote container down
```
This removes the container; the workspace directory and the saved settings stay. To remove the command too, run `termote uninstall` (see [Native](/installation/native/#uninstall)).
## Development
[Section titled “Development”](#development)
`docker-compose.yml` in the repository is for development from a checkout only. Everyone else uses `termote container up`.
# Native Installation
> Run Termote as a service of your user on Linux, macOS or Windows, with access to Claude Code, git and every other tool installed on the host.
The native server runs on the host, so its terminals see your tools: `claude`, `git`, `gh` and the rest. termote streams the terminal itself (PTY on Unix, ConPTY on Windows) into xterm.js in the PWA; there is no separate terminal process to install.
## Prerequisites
[Section titled “Prerequisites”](#prerequisites)
termote needs one terminal backend: **tmux** (on Windows, **psmux**) or a running **[Herdr](https://herdr.dev/#install)** server (see [Herdr Backend](#herdr-backend)); you do not need both. On Linux and macOS, **[Zellij](https://zellij.dev/documentation/installation)** 0.45.0 or later is a third choice (see [Zellij Backend](#zellij-backend)). **[herdr-core](https://herdr.dev/core/)** (`hc`) 0.1.16 or later, the core the herdr app talks to, is a fourth, on every OS (see [herdr-core Backend](#herdr-core-backend)).
Files and Changes use your **git**; following a link into another work tree of the same repository needs git 2.36 or later (Ubuntu 22.04 ships 2.34). With an older git such links are not followed.
* Linux
```bash
# tmux
sudo apt install tmux
# or Herdr instead: see https://herdr.dev/#install
# or Zellij 0.45+: see https://zellij.dev/documentation/installation
```
* macOS
```bash
# tmux
brew install tmux
# or Herdr instead: see https://herdr.dev/#install
# or Zellij 0.45+: brew install zellij
```
* Windows
```powershell
# psmux: tmux-compatible multiplexer for Windows
winget install psmux
```
### tmux Configuration (Recommended)
[Section titled “tmux Configuration (Recommended)”](#tmux-configuration-recommended)
With the tmux backend, for copy/paste to work with the keyboard toolbar buttons, add this to your `~/.tmux.conf`:
```bash
# Vi mode for copy: Space=start selection, Enter=copy
set-option -g mode-keys vi
# Mouse support (scroll, select) - toggle with Ctrl+b m
set-option -g mouse on
bind m set -g mouse \; display "Mouse #{?mouse,on,off}"
```
Apply the config:
```bash
tmux source-file ~/.tmux.conf
```
This enables: **Copy icon** → navigate → **Space** → select → **Enter** → **Paste icon**
Copy to System Clipboard
With mouse ON, tmux captures mouse events. To copy to system clipboard:
* **Hold Shift** while selecting text (bypasses tmux, uses terminal selection)
* Or press **Ctrl+b m** to toggle mouse off temporarily
## Installation
[Section titled “Installation”](#installation)
* Linux / macOS
```bash
curl -fsSL https://termote.ohnice.app/install.sh | sh
termote start
```
* Windows (PowerShell)
```powershell
irm https://termote.ohnice.app/install.ps1 | iex
termote start
```
* Homebrew (macOS, Linux)
```bash
brew install lamngockhuong/tap/termote
termote start
```
A Homebrew install updates with `brew upgrade termote && termote restart` instead of `termote update`, and is removed with `termote uninstall && brew uninstall termote`. Keep one install per machine: the installer’s and brew’s register the same service.
Then open <http://127.0.0.1:7680> (Windows: <http://127.0.0.1:7690>). Pinning a version and the file locations are covered in [Getting Started](/getting-started/).
Use `127.0.0.1` rather than `localhost`: termote does not listen on IPv6 `::1`, which a browser tries first for `localhost`. Where that attempt hangs instead of failing (Windows with WSL set to `networkingMode=mirrored`), every new connection waits for it, about 300 ms on each tab switch. A page opened through `localhost` shows a notice with a link to the same page at `127.0.0.1`. That is another address to the browser: you sign in again there, settings and unsaved drafts stay with `localhost`, and an installed app has to be installed again.
## What `termote start` Does
[Section titled “What termote start Does”](#what-termote-start-does)
1. **Picks the backend** the first time: Herdr when its server answers on its socket (`$HERDR_SOCKET_PATH`, else `herdr.sock` in Herdr’s config directory: `$XDG_CONFIG_HOME/herdr`, or `~/.config/herdr`; on Windows `%APPDATA%\herdr`, reached as a named pipe), tmux (psmux on Windows) when it is installed. When both are there it asks, or, without a terminal to ask in, picks tmux and prints how to switch. When neither is there it stops with install hints. The choice is saved and not detected again unless you pass `--mux`. Zellij and herdr-core are never detected: choose them with `--mux zellij` or `--mux hc`, or the menu (`termote` with no command).
2. **Creates the admin password** the first time (or with `--fresh`): type one, or press Enter to generate one. It is printed once, and only when `start` runs in a terminal: run by a coding agent, a script or into a pipe, `start` prints the username alone. `termote show-password` shows it again later.
3. **Checks the port and Tailscale first.** If anything fails, a server that is already running is left running, so a mistake does not cut the connection you may be using.
4. **Registers `termote serve` with your service manager** and starts it (see the table below), waits until the server reports this version, then prints the URLs, the allowed host names and the credentials. It always warns when auth is off or LAN access is on.
| OS | Service manager | Behaviour |
| --------------------------------- | --------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Linux | systemd user unit `~/.config/systemd/user/termote.service` | Restarted after a crash; tmux and its shells survive stop, restart and crash. Runs while you are logged in: `loginctl enable-linger $USER` keeps it running after logout and starts it at boot (`start` prints this hint) |
| Linux without user systemd (WSL2) | Detached process with a PID file | Not restarted after a crash or reboot; `start` says so. Run `termote start` again, or enable systemd in WSL (`[boot]` `systemd=true` in `/etc/wsl.conf`) |
| macOS | launchd agent `~/Library/LaunchAgents/app.ohnice.termote.plist` | Restarted after a crash, started at login; tmux survives |
| Windows | Scheduled Task `Termote` at logon, run hidden | No administrator rights needed. If policy refuses the task, a hidden launcher in the Startup folder is used instead (no restart after a crash then) |
The unit, plist or task never contains the password or any `TERMOTE_*` variable.
Windows
The Windows service setup is new in 1.0. Please report anything that does not work on [GitHub](https://github.com/lamngockhuong/termote/issues).
## Start Options
[Section titled “Start Options”](#start-options)
Every option is saved. A flag you leave out keeps its saved value, so `termote start` with no flags starts the server exactly as last time. A boolean option is turned off with `=false`.
| Option | Description |
| --------------------------------- | --------------------------------------------------------------------------------------------- |
| `--port <port>` | Port (default 7680; Windows 7690) |
| `--lan[=false]` | Listen on every interface instead of `127.0.0.1` only |
| `--tailscale <host[:port]>` | Publish over [Tailscale HTTPS](/installation/tailscale/) (default 443) |
| `--no-tailscale` | Stop publishing over Tailscale |
| `--cloudflare <host>` | Hostname of a [Cloudflare Tunnel](/installation/cloudflare/) you run (not with `--no-auth`) |
| `--no-cloudflare` | Forget the Cloudflare hostname |
| `--proxy-client-header <name>` | Header a proxy sets to the client’s address (default with `--cloudflare`: `CF-Connecting-IP`) |
| `--no-proxy-client-header` | Stop reading it |
| `--no-auth[=false]` | Disable the login |
| `--mux <tmux\|herdr\|zellij\|hc>` | Terminal backend (default: detected the first time; zellij: Linux, macOS; hc: herdr-core) |
| `--allow-host <name>` | Allow another `Host` header name (repeatable, no wildcard) |
| `--remove-host <name>` | Remove an allowed name (repeatable) |
| `--allow-local-user <name>` | Let another OS user of this machine connect, e.g. a reverse proxy |
| `--remove-local-user <name>` | Stop letting that user connect (repeatable) |
| `--allow-path <dir>` | Offer a folder in Files, under [Places](/usage/files/#places) (repeatable) |
| `--remove-path <dir>` | Remove an allowed folder (repeatable) |
| `--allow-herdr-no-auth` | Required together with `--mux herdr --no-auth` |
| `--allow-hc-no-auth` | Required together with `--mux hc --no-auth` |
| `--user <name>` | Login username (default `admin`; shared with the container) |
| `--fresh` | Set a new password |
```bash
termote start --lan # also reachable from the LAN (saved)
termote start # same options as last time
termote start --lan=false # back to this machine only
termote start --allow-host box.local # accept another host name
termote start --remove-host box.local
termote start --allow-path ~/notes # edit a folder outside the pane in Files
termote start --fresh # new password
termote start --user alice # log in as alice instead of admin
```
The allowed host names are always loopback, the Tailscale name, the `--allow-host` names, and with `--lan` the address a request arrived on (so it keeps working when the LAN IP changes). A request for any other name gets a 403 that names the `termote start --allow-host <name>` to run.
### Other users of this machine
[Section titled “Other users of this machine”](#other-users-of-this-machine)
On Linux and Windows the server refuses every connection made from this machine by another OS user, whether it goes to `127.0.0.1` or to the machine’s LAN address: your own user, root/SYSTEM and the users you allow are let in. A reverse proxy that runs as its own user (nginx as `www-data`, Caddy as `caddy`) needs allowing; the server log names the command when it refuses one. On Windows a proxy that runs as a service is let in when it is the service’s own process and your user cannot read its token (one whose token your user can read counts as its account, to allow like any other user); one that a wrapper such as NSSM or WinSW starts is refused: register the proxy itself as the service, or run it as your user. A service your user cannot open at all is refused too: for a standard user that includes `tailscaled`, so [`--tailscale`](/installation/tailscale/) does not work on native Windows.
```bash
termote start --allow-local-user www-data
termote start --remove-local-user www-data
```
macOS does not check the user of a connection, and WSL 2 lets every Windows user in. What else is left open: [Security Model](/usage/security/).
Give Termote a host name of its own
Browsers send a site’s cookies to every port of its host name, so another web service on the same name (another port on the same server, another app on the same domain) receives Termote’s session cookie whenever you open it, and with it a signed-in terminal. On a server that runs anything else, reach Termote under a name only it answers on: a Tailscale name, or a name of its own such as `termote.example.com`. Log out from the **More** menu on a device you no longer use.
## Herdr Backend
[Section titled “Herdr Backend”](#herdr-backend)
Linux, macOS and Windows can drive Herdr workspaces instead of tmux (psmux on Windows). Herdr must be installed on this host ([install Herdr](https://herdr.dev/#install)); Termote talks to its socket, and starts `herdr server` itself when it is not running. When Herdr is running at the first `start` it is picked for you; otherwise:
```bash
termote start --mux herdr
```
Herdr behavior differs from tmux
* Selecting a tab or pane on the PWA does **not** change what is shown on the Herdr desktop — it only changes which pane the client streams. There is no “show on desktop” button.
* Each pane gets an agent-status badge (idle/working/blocked/done) refreshed at most once per the configured poll interval; it is not pushed instantly.
* Herdr need not be running. When you open the page and nothing answers on its socket, Termote starts `herdr server` in the background; the page connects within a few seconds (the first 15 s after the server starts are left to you or to a service that starts Herdr, and after a failed start Termote waits 30 s, doubling to 10 minutes). Devices that can only view never start it. There is no option to turn this off.
* A Herdr started this way keeps running after `termote stop`, `restart` and `update`, like tmux, and `termote stop` does not stop it. While a full page is open, a Herdr stopped with `herdr server stop` is started again. Its output goes to `herdr-autostart.log`, shown by `termote logs` and `termote logs follow`, not by `termote logs server`.
* Its panes get the service’s environment: variables set by a login session or terminal (`SSH_AUTH_SOCK`, `DISPLAY`/`WAYLAND_DISPLAY`, sometimes `DBUS_SESSION_BUS_ADDRESS`, `COLORTERM`) may be missing, while variables from your shell’s rc files are there. Start Herdr yourself from that session to keep them.
* On a machine where Herdr is installed but not running, with no tmux or psmux, the first `termote start` still says no backend is found: run `termote start --mux herdr`.
* This is the Herdr on your host. The container runs its own Herdr instead, see [Container Installation](/installation/docker/#terminal-backend).
* `--mux herdr --no-auth` is refused unless `--allow-herdr-no-auth` is also given: Herdr exposes every workspace on the host, not just the pane the PWA is showing, so running it without a password needs an explicit opt-in.
## Zellij Backend
[Section titled “Zellij Backend”](#zellij-backend)
Linux and macOS can drive your [Zellij](https://zellij.dev/documentation/installation) sessions instead of tmux. It needs Zellij 0.45.0 or later (tested with 0.45.1); `start` checks that `zellij` is on your PATH and its version. Zellij is never picked for you:
```bash
termote start --mux zellij
```
A group is a Zellij session, a tab a Zellij tab and a pane a terminal pane (plugin panes are not shown). Every running session of yours is listed, as with tmux. `start` passes `ZELLIJ_SOCKET_DIR`, `XDG_RUNTIME_DIR` and, on macOS, `TMPDIR` from your shell to the service, so it sees the same sessions as that shell; run `termote start` from the shell whose sessions you want.
Zellij behavior differs from tmux
* The terminal is a real Zellij client: Zellij draws its own tab bar and status bar, and a tab takes the size of the smallest client viewing it.
* Selecting a tab on the PWA does **not** move your own Zellij client; each device selects its own tab.
* Termote’s client is in Zellij’s locked mode, so the toolbar’s Ctrl keys (Ctrl B, P, N, T, O, Q…) reach the pane. Your own locked-mode bindings still act (the default config’s Ctrl g unlocks).
* There is no server-side scrolling: the mouse wheel scrolls through Zellij itself, for every client viewing that pane. Select text still shows the pane’s history.
* A new tab needs a client attached to the session (a terminal, or Termote showing it); otherwise you see “open this Zellij session in a terminal (or in Termote) before adding a tab”.
* Closing a session from Termote deletes it: Zellij cannot resurrect it.
* On Windows, native `--mux zellij` is refused; use `termote container up --mux zellij`, see [Container Installation](/installation/docker/#terminal-backend).
## herdr-core Backend
[Section titled “herdr-core Backend”](#herdr-core-backend)
[herdr-core](https://herdr.dev/core/) keeps terminals running for the herdr app and any other client. With `--mux hc`, Termote is one more of those clients: it shows the spaces, tabs and panes the herdr app shows on this machine, as the app arranges them. It needs `hc` 0.1.16 or later on your PATH (its releases carry a checksum, not a signature); `start` checks the version and saves where `hc` is. It is never picked for you:
```bash
termote start --mux hc
```
* **The herdr app does not need to run.** Termote reads the app’s layout when there is one, and otherwise places the terminals by their labels. A tab, split or close made in Termote shows in the app within about a second, and the other way round.
* **Only the herdr app’s terminals show.** A terminal another program started in the core, without the app’s labels, stays hidden, as it does in the app.
* **Typing from a phone does not resize the desktop’s pane.** The pane keeps the desktop’s size unless you turn on **Fit herdr pane to this device** in Settings.
* **Agent status** comes from the agent itself (Claude Code reports `working`, `blocked` and `done`). An agent you look at in Termote is no longer “done” in the app, and the other way round.
* **A new group takes no name**: the app names a space after its folder, so Termote asks only for the directory, and a group cannot be renamed. Zoom, swap, resize and pane names are left to the app.
* **The core is started for you** when you open the page and it does not run, the way Termote starts Herdr. A core Termote started runs with the service’s environment, and terminals the app opens afterwards inherit it. `termote stop` and `termote uninstall` never stop the core.
* `start` saves `HC_SESSION` and `HC_SOCKET` from your shell in the config, so the service talks to the same core as that shell; a later `start` from a shell without them keeps the saved values.
herdr-core has no login of its own
Anyone who can reach its socket drives every terminal, so Termote never runs it in a container (`container up --mux hc` is refused), and `--mux hc --no-auth` is refused unless `--allow-hc-no-auth` is also given. Terminals of other machines connected to the core are not shown.
## Managing the Server
[Section titled “Managing the Server”](#managing-the-server)
```bash
termote status # what the running server reports (version, backend, pid, Via)
termote signins # browsers signed in with the password; signins revoke <id>|--all
termote stop # stop it; it starts again at the next login
termote restart # stop and start with the saved options
termote logs # recent lines of every log
termote logs server # server log only
termote logs follow # tail live (Ctrl+C to stop)
termote logs clean # delete the log files
termote show-password # print the saved username and password
```
The CLI talks to the server over a socket only your user can open (`~/.local/state/termote/run/<port>.sock`, a named pipe on Windows), without sending the password; `termote status` shows `Via: socket`. Against the container or an older server it uses `127.0.0.1` and the saved password (`Via: TCP`), except the native server on Windows, which gets the password over its pipe only (`termote status` then says the pipe did not answer).
The service runs `termote serve`, which reads the saved config. Once a config file exists, `TERMOTE_*` environment variables are ignored, and `serve` removes them from its environment so no terminal inherits one.
## Updating
[Section titled “Updating”](#updating)
```bash
termote update # newest stable release (1.0 or later)
termote update --version 1.0.1 # a specific version
termote update --version 1.13.0-rc.1 # a pre-release, only when named
termote update --force # reinstall the current version
```
`update` downloads the release and checks its `.sha256` and, from 1.10.0 on, the release signature: an archive the Termote release key did not sign is refused even when its checksum matches. Without `--version` it never goes back from a signed release to one before 1.10.0; name that version to install it. It installs it beside the current version, switches to it, restarts the service and waits until the server reports the new version and keeps answering. If it does not, it switches back to the previous version and restarts that one. The current and the previous version are kept; your config and the service registration do not change. It works when typed inside a Termote pane.
`update` refuses to run from a git checkout (use `git pull && make build` there) and for a binary that the installer did not put in place.
## Uninstall
[Section titled “Uninstall”](#uninstall)
```bash
termote uninstall # keeps your config, saved password, logs, recent group directories and deleted files
termote uninstall --purge # removes them too
```
This removes the service registration, Termote’s Tailscale mapping, the `termote` command, the install root and the images you uploaded from the app (a cache). Your config (`~/.config/termote`) and logs (`~/.local/state/termote`) stay, so a reinstall keeps your password and your devices stay signed in; `uninstall` prints both paths. On Windows, `%APPDATA%\termote` and `%LOCALAPPDATA%\termote\state` stay. Add `--purge` to remove them as well; the interactive menu asks.
The files you deleted from the Files view stay as well (they move to a trash next to the uploads, such as `~/.cache/termote/trash`), since they are your files and not a cache; `uninstall` prints the folder. `--purge` removes it too.
On Windows, the files of the running `termote.exe` are removed a moment after the command exits.
## Prevent macOS Sleep
[Section titled “Prevent macOS Sleep”](#prevent-macos-sleep)
macOS automatically sleeps after inactivity, disconnecting remote terminal sessions. Use `caffeinate` to keep the machine awake while Termote is running:
```bash
# Prevent system sleep (runs until you press Ctrl+C)
caffeinate -s
# Or run in background
caffeinate -s &
# Stop background caffeinate
pkill caffeinate
```
Tip
Use `caffeinate -s` (system sleep only) instead of `-d` (display sleep) — you don’t need the display on for remote access.
## Troubleshooting
[Section titled “Troubleshooting”](#troubleshooting)
### Quick Diagnostics
[Section titled “Quick Diagnostics”](#quick-diagnostics)
```bash
termote status # is it running, which version and backend
termote logs server # last server log lines
```
### Common Issues
[Section titled “Common Issues”](#common-issues)
| Issue | Solution |
| ------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `termote: command not found` | Open a new terminal, or add `~/.local/bin` to your PATH as the installer printed |
| No terminal backend found | Install tmux (Windows: `winget install psmux`) or Herdr, then `termote start --mux herdr` |
| `zellij not found` / `is too old` | Install Zellij 0.45.0 or later, then `termote start --mux zellij` |
| Zellij sessions missing | Run `termote start` from the shell that sees them (`zellij list-sessions`); EXITED ones are not listed |
| `zellij_version_mismatch` after an upgrade | Restart the Zellij sessions of the old version (or Termote) |
| `zellij_socket_dir` | The Zellij socket dir is not private to you (the log says why): `chmod 700` it, or set `ZELLIJ_SOCKET_DIR` to a private dir |
| Port in use | `start` names the port; stop the other program or choose `termote start --port <port>` |
| Host rejected (403) | Run the `termote start --allow-host <name>` the error names |
| Reverse proxy gets no answer | The log shows `local-user-refused`: run the `termote start --allow-local-user <name>` it names |
| Saved password cannot be decrypted | The `secret` file was removed or replaced: `termote start --fresh` |
| Stops when you log out (Linux) | `loginctl enable-linger $USER` |
| Not running after a reboot (WSL2) | No user systemd there: `termote start` again, or enable systemd in WSL |
| No sessions, `mux command failed` (Windows) | `psmux list-windows -t main` prints nothing while `psmux ls` lists it: a filter is intercepting localhost traffic. In AdGuard, turn off “Filter localhost” |
| Stale PWA cache | More (⋯) menu → “Clear cache & reload” to refresh (it also signs out and drops unsaved file text) |
### tmux / psmux Session
[Section titled “tmux / psmux Session”](#tmux--psmux-session)
* Unix (macOS/Linux)
```bash
tmux list-sessions # List sessions
tmux attach -t main # Attach to session
tmux kill-session -t main # Reset session
```
* Windows (PowerShell)
```powershell
psmux list-sessions # List sessions
psmux attach -t main # Attach to session
psmux kill-session -t main # Reset session
```
### API Test
[Section titled “API Test”](#api-test)
The API is behind the login (`termote show-password` prints the password):
```bash
curl -u admin:<password> http://127.0.0.1:7680/api/mux/health # or the name set with --user
```
# Tailscale HTTPS
> Serve Termote over HTTPS on your tailnet with tailscale serve: certificates handled for you, no port opened to the Internet, and no sudo from Termote.
Automatic HTTPS using `tailscale serve`. No manual certificate management, and Termote never runs `sudo` for it.
## Prerequisites
[Section titled “Prerequisites”](#prerequisites)
* [Tailscale](https://tailscale.com) installed and authenticated
* MagicDNS and HTTPS certificates enabled for your tailnet
* **Linux only, once:** let your user change `tailscale serve` without sudo
```bash
sudo tailscale set --operator=$USER
```
If you skip this, `termote start --tailscale` stops before touching the running server and prints this line.
* **Native Windows:** not supported for a standard (non-administrator) user. That user cannot open the `tailscaled` process, so the server refuses the connections `tailscale serve` makes and the page answers 502.
## Setup
[Section titled “Setup”](#setup)
1. Start with the Tailscale name of this machine
* Native
```bash
termote start --tailscale myhost.tailnet-name.ts.net
```
* Container
```bash
termote container up --tailscale myhost.tailnet-name.ts.net
```
2. Open <https://myhost.tailnet-name.ts.net> from any device on your tailnet
The option is saved, so a later `termote start` (or `termote container up`) keeps publishing it.
## Options
[Section titled “Options”](#options)
```bash
# Another HTTPS port (default 443)
termote start --tailscale myhost.tailnet-name.ts.net:8443
# Tailscale + LAN
termote start --tailscale myhost.tailnet-name.ts.net --lan
# Stop publishing over Tailscale
termote start --no-tailscale
```
The same `--tailscale` and `--no-tailscale` options work with `termote container up`.
Tip
Tailscale is the recommended way to expose Termote securely over the internet (see the [security model](/usage/security/#recommendations)). To sign a phone in without typing the password, [pair it](/usage/devices/) with a one-time code.
## How It Works
[Section titled “How It Works”](#how-it-works)
```plaintext
Tailnet device → Tailscale (HTTPS) → 127.0.0.1:7680 → termote (terminal stream + API)
```
* **Native:** the service’s `termote serve` applies `tailscale serve --bg --https=<port> http://127.0.0.1:<port>` every time it starts, at boot, on restart and after an update, so the mapping always follows the saved config. It removes the mapping again when it stops, so while the server is not running (you logged out, it was stopped) the Tailscale name leads nowhere, rather than to a free port another user of the machine could listen on.
* If that HTTPS port on Tailscale already serves something else, `start` refuses and suggests another port (`--tailscale myhost.tailnet-name.ts.net:8443`); the server never replaces a mapping that is not its own.
* **Container:** `container up` applies the same mapping on the host; `tailscaled` keeps it across reboots.
* The Tailscale name is added to the Host allowlist automatically, so requests through `tailscale serve` are not rejected by the DNS-rebinding guard.
* `termote stop`, `termote start --no-tailscale`, `termote uninstall` and `termote container down` remove only Termote’s own mapping (`tailscale serve --https=<port> off`), never your other `tailscale serve` entries.
Tailscale handles:
* TLS certificates (auto-renewed)
* Authentication (Tailscale ACLs)
* NAT traversal
## Stalls on mobile data
[Section titled “Stalls on mobile data”](#stalls-on-mobile-data)
Termote works on Wi-Fi, but on mobile data the header stays at **Loading…** with 0 sessions, or the terminal stays blank, while the Chat view may still load. The PWA shows a toast saying the network seems to drop large replies.
Some carriers’ IPv6 paths carry packets of at most 1280 bytes. Tailscale’s tunnel is 1280 bytes inside, and WireGuard adds 80 bytes on IPv6, so a full-size packet is 1360 bytes on the carrier’s network and is dropped without an error coming back. Small replies (health, a short chat poll) get through; a session list, a terminal redraw or an image upload never does.
Check it from the server while the phone is on mobile data (`100.x.y.z` is the phone’s Tailscale address, `tailscale status` lists it):
```bash
ping -c 3 -M do -s 1172 100.x.y.z # 1200-byte packets: replies
ping -c 3 -M do -s 1252 100.x.y.z # 1280-byte packets: 100% loss on an affected network
```
Fix it by lowering Tailscale’s MTU on the server to 1200 (Linux, `tailscaled` as a systemd service):
```bash
echo 'TS_DEBUG_MTU=1200' | sudo tee -a /etc/default/tailscaled
sudo systemctl restart tailscaled
ip link show tailscale0 # mtu 1200
```
`tailscale serve` handles the phone’s TCP connection inside `tailscaled`, so a smaller MTU there makes both directions fit, uploads included; Termote needs no change.
Caution
With an MTU below 1280, Linux removes the IPv6 address from `tailscale0`: this machine is then reachable over its Tailscale IPv4 address and MagicDNS name only, and HTTP/3 (QUIC) does not run over Tailscale. Traffic outside Tailscale is not affected. The setting lives in the system config, not in Termote, so set it again after moving to another machine or reinstalling Tailscale.
# Agent Chat
> Read and answer Claude Code and Codex from a chat view on your phone instead of the terminal: messages, images, slash commands and permission dialogs.
Long replies are hard to read in a terminal on a phone, and so is typing a prompt into one. When a pane runs **Claude Code** or **Codex**, the header shows a **Terminal / Chat** switcher (on a phone, one view button that opens a menu of views). The Chat view is a second way to look at the same session: what you send there is typed into the pane, and what the agent writes shows up in both views. Nothing runs in the background on its own. Codex works the same way, with its own requirements and one difference for dialogs (see [Codex](#codex)).

## Requirements
[Section titled “Requirements”](#requirements)
* Claude Code running in a pane, under one of these backends:
* **tmux** on Linux or macOS
* **psmux** on Windows
* **Herdr** on any OS, after running `herdr integration install claude` once
* A native install (`termote start`). The container image does not include Claude Code.
* For Codex: see [Codex](#codex).
Setting it up from scratch? Follow [Control Claude Code from your phone](/guides/claude-code-from-phone/).
## What the Chat View Shows
[Section titled “What the Chat View Shows”](#what-the-chat-view-shows)
* The whole conversation of the current session, read from the transcript Claude Code writes to disk: your messages, replies rendered as markdown, and tool calls. A command shows as `$ command` with its output folded behind `Output · N lines` (opened, the last 10 lines; a failed one is open already). Three steps or more in a row fold into one line such as `3 commands, 2 reads`; the steps the agent is running now stay open on the newest ones.
* New turns within about two seconds. After `/clear` or a resume, the view switches to the new session without mixing the two.
* While a turn runs, a row under the conversation shows what the terminal’s working line says: the agent’s word for it (`Incubating…`, `Working…`), whether it is thinking or running hooks, how long the turn has run and the tokens received. The agent writes a reply to its transcript only once the reply is complete, and never writes its thinking, so this row is what moves until then. When the line cannot be read, the row still says the agent is working and for how long. Once Claude Code ends a turn with background commands still running, the row says what is left (`Turn done · 2 shells still running`) until they finish and the agent carries on.
* While a Claude Code or OpenCode session has subagents running, a strip above the composer says how many (`2 subagents running`) and lists them as the terminal does: each one’s type, description, the last tool it called, how long it has run and the tokens of its last request. A subagent started by another is indented under it. Tap the strip to fold it; it goes away once none runs. The [overview](/usage/overview/) lists the finished ones too.
* Code blocks framed with their language, highlighted, with a Copy button. A ` ```mermaid ` block is drawn as a diagram, with the same Show source, export and full-screen buttons as the [Markdown preview](/usage/file-previews/#markdown-preview); one that cannot be drawn stays code with the reason.
* File paths as links to the pane’s [Files view](/usage/files/): the path on a tool card, inline code that names a file (`src/app.ts`, `./run.sh`, `src/app.ts:42`, `src/app.ts#L42`) and a relative Markdown link. Tapping one opens that file in Files, at the line when one is given. A relative path is taken from the pane’s directory, then from its root; a path outside the root is never requested and stays plain text, as every path does in a pane without a Files view. On desktop the file opens in the side panel and the Chat stays where it is. A single name under `/`, such as `/clear`, is a slash command, not a file.
* Images in a reply as text links. They are never loaded, so a reply cannot make your browser contact another site.
## Sending a Message
[Section titled “Sending a Message”](#sending-a-message)
Type in the box at the bottom and tap the send button (Ctrl+Enter on desktop). To send with Enter alone, pick **Enter** under **Settings → Keyboard → Send messages and comments with**; Shift+Enter then starts a new line. Multi-line messages arrive as one turn, exactly as written.
Termote only sends when the screen shows Claude Code’s input box empty. While Claude Code works, the message still goes: Claude Code puts it in its own queue and reads it when it is ready, and the Chat view says `Queued: Claude Code reads it when it is ready.` In the terminal it shows as a queued message, which you can edit there (Up). Otherwise the message is refused and the pane gets no key at all:
| Message | Code | Meaning |
| ------------------------------------------------------------- | ------------------------- | --------------------------------------------------------------------------------------------------------------------------------------- |
| Not sent: the agent is waiting for an answer | `input_not_ready` | A dialog is open |
| Not sent: the terminal’s input box already holds text | `input_has_draft` | Something else is typed in the terminal’s input box; send or clear it there, or press **Clear it and send** (not while the agent works) |
| Not sent: the pane is in copy mode | `input_not_ready` | The tmux pane is scrolled back in copy mode; leave it (`q`) in the terminal |
| Not sent: the text did not show in the input box | `paste_not_confirmed` | Enter was not sent; the text may be in the input box now. Sending the same message again submits it |
| The text was pasted but not submitted | `delivered_not_submitted` | The text is in the input box but Enter was not sent; check the terminal before sending again |
| The conversation changed; it has been reloaded | `session_changed` | The pane runs another session now (`/clear`, a resume); send again if you still want to |
| Too long: the limit is 16 KB. | `text_too_long` | The message is over 16 KB (UTF-8); split it |
| Not sent: the input box holds part of this message | `partial_paste` | The input box may hold some of the message’s images or text; check and clear it there |
| Not sent: an image is no longer on the host | `invalid_request` | The host deleted the upload (older than 7 days, or storage full); remove it and attach again |
| Not sent: an earlier message to this pane is still being sent | `input_busy` | Earlier messages to the same pane took too long to go in; nothing was typed, send it again once they are in |
Every refusal that leaves something to check offers **Open terminal**.
### Attaching Images
[Section titled “Attaching Images”](#attaching-images)
Tap the image button next to the box to pick a photo (or take one on a phone), or paste an image into the box. Each image is uploaded to the host at once and shows as a thumbnail above the box; the send button waits until every upload has finished. Up to 5 images go with one message, with or without text. Each image also puts an `[Image #N]` token in the box where the caret is, as Claude Code does, so you can write “this one \[Image #1], that one \[Image #2]”. Remove an image with the cross on its thumbnail or by deleting its token.
Termote pastes the message into Claude Code (or Codex) in order: the text up to an image, then that image’s path on its own, waiting each time for the agent to show it (an image as `[Image #N]`), and finally presses Enter, so the images reach the agent as attachments where you put them. A `/` command keeps its images where you put them, in its arguments. A message that starts with `!` gets its images first, so it is never run as a shell command. If a step fails halfway, the input box is cleared only when it holds nothing but what this message pasted; otherwise you get `partial_paste` and the box is left for you to check.
The images are stored on the host as described in [Attach an Image](/usage/keyboard/#attach-an-image), and are kept after the message is sent. The image button is hidden when the server cannot store uploads, and in view-only mode. A copy that holds both an image and text pastes the text.
## Slash Commands
[Section titled “Slash Commands”](#slash-commands)
Type `/` at the start of a message and a list of commands opens above the box. Keep typing to narrow it: names that start with what you typed come first, then any whose name or description contains it. Each row shows where the command comes from:
* `built-in`: Claude Code’s own commands, with `/exit` first.
* `project`: `.claude/commands/` and `.claude/skills/` in the pane’s project (the folder the Files view shows).
* `user`: `commands/` and `skills/` in your Claude config dir (`~/.claude`, or `CLAUDE_CONFIG_DIR`).
* `plugin`: commands and skills of the plugins you have installed and enabled, named `/<plugin>:<name>`. A plugin turned off in the project’s `.claude/settings.json` or `.claude/settings.local.json` is left out. A plugin bundle placed in your config dir’s `skills/` (a folder holding `.claude-plugin/plugin.json`, as some kits install themselves) is listed the same way, named after its manifest; its links may also lead into a hidden folder right under your home, such as `~/.<kit>`.
Skills carry a `skill` tag as well.
Tap a row, or press Enter or Tab, and the box becomes `/name `so you can type arguments; it is not sent until you send it. The arrow keys move through the list and Escape closes it, as does a space after the name.
A few commands open a screen the Chat view cannot drive, such as `/model`, `/config`, `/permissions`, `/resume` and `/mcp`. They are marked `opens in Terminal`: picking one sends it and switches to the terminal. `/exit` asks before it is sent, since it ends Claude Code; the app then goes back to the terminal.
Only names and descriptions are read, never what a command contains. A skill installed as a symlink is listed when it leads into the project, the config dir or `~/.agents/skills`; a link to anywhere else is not, and neither are MCP prompts. Any command can still be typed in full.
### Prompt Templates
[Section titled “Prompt Templates”](#prompt-templates)
Prompts you type again and again can be saved as templates, with variables such as the file shown or the text you selected, and inserted from the **Templates** button, the `/` list or the command palette: see [Prompt Templates](/usage/prompt-templates/). An inserted template is never sent for you.
## Answering Dialogs
[Section titled “Answering Dialogs”](#answering-dialogs)
When Claude Code asks for permission or asks a single-choice question (up to nine options), a card replaces the message box. Each button picks that option; **Cancel** sends Escape. If the screen changed before your answer was sent, or another device answered first, nothing is sent and the card shows the dialog that is open now.
The card shows the whole dialog, however long. A long command or question sits in a box you can scroll, with **Show all** to open it; the rest of the card scrolls too, and **Cancel** stays at its foot. On a permission card the buttons that approve stay off until you have read the whole command (open it with **Show all**, or scroll to its end); the one that refuses works at once.
A single-choice question keeps its buttons, `Chat about this` included. Its `Type something` option is an **Other…** button: it opens a field for an answer of your own (one line, up to 1 KB), sent with **Send**. Termote moves the pointer to `Type something` in the terminal, types the text there, checks that it shows, then presses Enter. If anything on screen is not what it expects, it stops and the card shows the dialog that is open now; if the text did not show, it may be left in the terminal’s field, so check it there. **Cancel** under the field only closes it: the dialog stays open. A line break or a control character is refused, not removed.
A question in several parts (a row of tabs such as `Size · Drink · Submit`) is answered one tab at a time. The card shows the tabs as steps, with the one open marked; each answer moves Claude Code to the next tab, which gets a card of its own, up to the Submit tab with **Submit answers** and **Cancel**. Tap another step to go to that tab, to skip a question or come back to one. If you change tabs in the terminal before tapping, nothing is sent and the card shows the tab that is open now.
When several choices can be ticked at once, each button ticks or unticks one option, and **Next** moves on with the options you ticked. **Other…** there adds your text as one more option, already ticked; its button unticks it like the others.
A question that shows a preview beside its options is answered the same way: one tap picks the option. The preview itself is only shown in the terminal.
## Codex
[Section titled “Codex”](#codex)
A pane running Codex gets the Chat view with its conversation: your messages, replies rendered as markdown, commands with their output, file changes as diffs. You send a message from the box at the bottom, under the same rules as for Claude Code: Termote only sends when the screen shows Codex’s input box empty, and a refusal leaves the pane untouched. While Codex works, the message is sent with Tab, which queues it for after the turn (Codex lists it under “Queued follow-up inputs”), instead of Enter, which would steer the turn it is running. A `/` command is not queued: send it once Codex is done.
Approval dialogs (run a command, make file edits) depend on the backend:
* **Herdr**: Herdr reports the agent as blocked, so the dialog is a card with a button per option. **Cancel** sends Escape.
* **tmux**: Codex’s session file records no approval request, so Termote cannot tell that a dialog is waiting for you. The card shows the dialog read only and says “Answer this dialog in the terminal.”
Other Codex dialogs (model picker, rate-limit prompt, and so on) are always shown read only, on either backend.
### Codex Commands and Skills
[Section titled “Codex Commands and Skills”](#codex-commands-and-skills)
Type `/` at the start of a message for Codex’s own commands (checked against Codex 0.162.0), with `/exit` first. They work as for Claude Code: a pick becomes `/name `for its arguments, commands that open a screen of their own (`/model`, `/new`, `/resume`, `/status`, `/diff` and others) are marked `opens in Terminal`, and `/exit` and `/logout` ask before they are sent, since both end Codex. Typed in full, the aliases `/quit`, `/cwd`, `/clean` and `/pet` behave as `/exit`, `/pwd`, `/stop` and `/pets`. Codex has no custom `/` commands: its custom prompts are gone, replaced by skills.
Skills come after `$`, where Codex itself looks for them, anywhere in a message: type `$` at the start of a word and a list of skills opens above the box, narrowed by what you type after it. A pick replaces that word with `$name `and leaves the rest of the message as it was; the skill runs when you send the message. A message that ends with `$name` is sent with a space after it: Codex’s own list would otherwise open on that word and take the Enter that submits it. Each row shows where the skill comes from:
* `project`: `.codex/skills/` and `.agents/skills/` at the top of the pane’s project and, in a git repository, in each folder from there down to the pane’s current folder, whether or not you have trusted the project in Codex (Codex reads them either way).
* `user`: `skills/` in Codex’s home (`~/.codex`, or the `CODEX_HOME` of the Codex in the pane), then `~/.agents/skills`.
* `system`: the skills Codex ships with (`skills/.system/` in its home), then `/etc/codex/skills` on Linux and macOS. `[skills.bundled] enabled = false` in `config.toml` turns the first ones off.
* `plugin`: the skills of the Codex plugins turned on in `config.toml` in Codex’s home and installed under its `plugins/cache/`, named `$<plugin>:<skill>` as Codex names them (for example `$atk:review`). An entry with `enabled = false`, `[plugins._default] enabled = false` for an entry that does not say, `[features] plugins = false`, or a `config.toml` Codex cannot read turns them off.
The list follows how Codex 0.164.0 loads skills:
* Skills are found in subfolders too, up to six folders down (for example `~/.agents/skills/group/deploy/SKILL.md`); folders whose name starts with `.` are skipped.
* A skill needs front matter between two `---` lines with a `description`; a `SKILL.md` without one is left out, as Codex leaves it out. The name is the front matter’s `name`, else the folder’s, and `user-invocable` (a Claude Code field) does not hide a skill.
* A skill in a folder that holds a plugin manifest (`.codex-plugin/`, `.claude-plugin/` or `.cursor-plugin/plugin.json`), or below one, is named `$<plugin>:<skill>`, as in a plugin.
* `[[skills.config]]` entries in `config.toml` turn a skill off by `name` or by the `path` of its `SKILL.md` (`~` and paths relative to Codex’s home work); a later entry wins.
A name found twice keeps the first one, in the order above but with the folders below the top of the project after the `system` ones, so a skill deep in the project (a vendored dependency, say) cannot stand in for one of the project’s or yours; a plugin’s skill wins over any other of its name. Only names and descriptions are read, and a skill installed as a symlink is listed only when it leads into the folders of its own row (the project, your two skill folders, `/etc/codex/skills`, or the plugin).
Codex may still run a different set than the list shows:
* Plugins from the `openai-*` marketplaces (curated, bundled, remote) are left out, since Codex loads them only for some sign-ins, and so are plugins or skill settings in a project’s `.codex/config.toml`, a profile or `-c`.
* A plugin or skill whose name has a character other than letters, digits, `_`, `-` and `:` is left out, since it cannot follow `$`. This includes names with accented letters or a `.`.
* Codex lists every skill of a name; the list shows one.
* The pane’s current folder is the folder of whatever runs in it at that moment, which can differ from the folder Codex started in. Without git, the pane’s folder counts as the top of the project.
* Not looked at: a skill symlinked from outside those folders, the `policy.products` of a skill’s `agents/openai.yaml`, front matter that ends past the first 8 KB, and front matter Codex cannot parse. A plugin manifest that is a symlink or larger than 1 MB does not name the skills below it.
* A very large skill folder is read in part (2,000 folders, two seconds).
Requirements:
* Codex run with `codex --no-daemon`. By default every Codex TUI hands its session to one shared background process, and Termote cannot tell which pane a session belongs to, so such panes have no Chat view.
* **tmux** on Linux or macOS, **psmux** on Windows, or **Herdr** on any of them after running `herdr integration install codex` once. On psmux the Chat view shows the conversation and its status, but sending from it does not work yet ([#408](https://github.com/lamngockhuong/termote/issues/408)).
* On Windows, Codex must run as you, not as administrator. A `CODEX_HOME` set in the pane is followed when it is a path on a drive letter.
* Checked with Codex 0.159.3, and with 0.162.0 on Windows. A later version that writes its session elsewhere loses the Chat view; on tmux the server log notes once that it found Codex without a session file.
What to expect:
* A Codex that has received no message yet has no conversation to show: Codex writes its session file with the first message. On tmux the Chat button appears then; on Herdr it is there from the start and says no session was found until then.
* After `/new`, the Chat view moves to the new conversation once you send it a first message (Codex creates its session file then). After `/resume`, it shows the resumed conversation right away, on every backend. Codex keeps the earlier sessions open, and Termote follows the one it sees Codex open or write to. When it could not watch the switch (Termote restarted, or `/new` and `/resume` both happened while no Termote page was open), the Chat view waits for your next message instead of guessing, so a message from it never lands in another conversation.
* On Herdr, a pane that started the shared background process before you switched to `--no-daemon` can keep reporting a session that is not its own, even another pane’s, until Codex restarts in it.
* Only the Codex process writing the session counts: another program reading the same session file (`tail -f`) gets no Chat view.
## OpenCode
[Section titled “OpenCode”](#opencode)
A pane running OpenCode 2.0 or later gets the Chat view with its conversation: your messages, replies rendered as markdown, its reasoning, and each tool call with its result. Its status (working, blocked on a permission or a question, idle) shows in the sidebar, Overview, the activity log and notifications. A turn that fails (a rate limit, a provider error) or that you interrupt shows as a note with OpenCode’s error message.
From the view you send messages (with pasted images), as Tasks and Review do too. A message sent while OpenCode works waits in its queue for the turn to end, as with Claude Code. While a turn runs, the working row shows how long it has run, and when the provider refuses a request (a rate limit) the attempt OpenCode retries and the provider’s error.
An image you pasted opens again from its chip, here and in an old session you read from History. A subagent OpenCode starts runs in a session of its own: the strip above the composer and the [overview](/usage/overview/) list them as for Claude Code, with the ones a subagent starts in turn under it. A session’s tokens and cost show on its card, in History and in the daily totals, with the cost OpenCode recorded at your provider’s prices (see [Tokens and cost](/usage/overview/#tokens-and-cost)).
A permission request shows as a card with **Yes, once**, **Yes, always** (with the patterns it saves) and **No**. A question, or a form a tool asks you to fill in, shows its fields (text, a number, a checkbox, options to pick one or several, other text where the question takes it) with **Send**; what is missing or out of range is marked before anything is sent. **Cancel** refuses a permission or dismisses a form. A form that asks you to open a link is answered in the terminal. The card is worded in the app’s language.
Type `/` at the start of a message for OpenCode’s commands, as its service lists them for the session’s folder: its own (`/goal`, `/init`, `/review`, tagged `built-in`) and those of your OpenCode config and the project (tagged `custom`). Skills come after `@`, as in OpenCode, anywhere in a message; each row shows whether a skill is OpenCode’s own (`system`), the project’s or yours (`user`). On send, Termote reads both lists again: `/name` runs that command only when it is listed, with the rest of the message as its arguments, and each listed `@name` attaches its skill. Any other `/name` reaches the model as text, and so does an `@` before a name no skill has (a file or an address). OpenCode’s other `/` commands (switching the model, theme or session) act on its terminal screen and are not offered: use them in the terminal. A view-only device gets no list.
OpenCode 2 comes from its GitHub releases or Homebrew; the `opencode-ai` package on npm is still OpenCode 1, which Termote does not read.
Termote reads OpenCode through its background service (the one `opencode` starts on its own), on this machine and for your own user. It never starts that service: when it does not run, the pane shows only OpenCode’s logo. Which session a pane shows is known:
* **Herdr**: from Herdr’s OpenCode integration (`herdr integration install opencode`), including a session you switch to inside OpenCode. After `/new` the pane keeps the previous session until you send the first message of the new one. Without the integration, as on tmux: from `--session <id>`, which an OpenCode started or resumed from Termote always has.
* **tmux, psmux, Zellij, herdr-core**: only when OpenCode was started with `--session <id>` (or `-s <id>`). A plain `opencode` shows the logo only, and a session you switch to from inside OpenCode is not followed: the view keeps the one named on the command line.
OpenCode run with `--standalone` or `--server` is not read. If you set `XDG_STATE_HOME` in your shell, set it for Termote’s service too, or Termote looks for OpenCode in `~/.local/state`.
An `opencode` opened while the service does not run starts one itself, as a child of that OpenCode; this includes an OpenCode that Termote starts or resumes for you. Termote cannot keep OpenCode from doing so.
On Windows the background service runs under the OpenCode window that started it: closing that pane while OpenCode still runs in it stops the service for every other OpenCode too. The next `opencode` you open starts a new one, and Termote finds it again within a few seconds.
## Starting an Agent (Herdr)
[Section titled “Starting an Agent (Herdr)”](#starting-an-agent-herdr)
On Herdr, a pane that shows only its shell (nothing running, not even a program started with `exec`) also offers the Chat view. Instead of a conversation it shows **Claude Code**, **Codex** and **OpenCode**: tap one and Termote asks Herdr to start it in that pane.
* Claude Code starts as `claude`, Codex as `codex --no-daemon`, OpenCode as `opencode --session <id>` with a new session id Termote makes, so the Chat view knows the session from the start. No other arguments, model or name can be given.
* Before typing the command, Termote presses Ctrl+C in the pane, so a half-typed line, a continuation prompt or a heredoc does not take the command. A pane running anything else is refused with nothing typed.
* While the agent starts, the buttons wait and the view says “Starting Claude Code…”. Once Claude Code is ready the conversation shows here; until Herdr reports its session (a few seconds) the view says it is starting.
* If the agent first asks something in the terminal (trusting a new folder, signing in), the PWA opens the Terminal view and tells you so. If it does not start (not installed, it exited), a warning offers **Open terminal** to see why. Herdr then holds that start for up to 30 seconds: starting again sooner says so, with nothing typed.
* Codex writes its session only with its first message, which the Chat view cannot send without one: type the first message in the terminal, and the Chat view follows from the next one. OpenCode likewise makes the session only with its first message, typed in the terminal.
* You can move to another pane meanwhile: a toast tells you how the start went.
Requirements: Herdr 0.8.2 or later on Linux, macOS or Windows (OpenCode: checked with Herdr 0.9.5; a Herdr that cannot start OpenCode says so). tmux and psmux do not offer it, and view-only mode hides it.
On Windows a pane counts as idle only when its shell has no child process, so a pane running `ping`, `nvim` or a nested `cmd` is refused with nothing typed. A shell that keeps a long-lived child (`Start-Job`, a GUI program started from it, something its profile launched) is refused the same way, and so is a `wsl.exe` pane. Work that runs inside PowerShell itself has no child process, so the pane looks idle: the start’s Ctrl+C clears a wait (`Start-Sleep`, `Read-Host`), but it also stops a `.ps1` script or a command such as `Copy-Item -Recurse` halfway, as pressing Ctrl+C there would. Start an agent only in a pane you know is at its prompt.
## Resuming an Old Session
[Section titled “Resuming an Old Session”](#resuming-an-old-session)
Every pane can list the Claude Code, Codex and OpenCode sessions that ran in its folder: the pane’s root (its git repository, as [Files](/usage/files/) finds it) or a folder under it, newest first. Open the list from any of these:
* the history button in the header of the Chat view;
* **Session history** in the command palette (`>` commands; no default shortcut, set one under Settings > Shortcuts);
* **Session history** in a group’s menu in the sidebar (it lists the folder of the tab the group opens on);
* a pane without an agent, whose Chat view shows its three latest sessions with a **Resume** button each.
Each row shows the agent, a title, when it was last active, its git branch and its folder under the root. The title is the one you gave it with `/rename`, else the one the agent made (Codex: the thread name; OpenCode: the title it made), else its first prompt, shown in italics. Search filters by title and branch; with more than one agent in the list, a filter picks one.
Tap a session to read it, drawn as the Chat view draws a running one; **Show earlier messages** loads older parts. Its title shows the tokens the session used and their estimated cost, and the top of the list adds up the folder’s sessions today and over the last 7 days (see [Tokens and cost](/usage/overview/#tokens-and-cost)). **Files changed**, under the title, lists the files the session changed, each opening its diff or the file (see [Files a Session Changed](/usage/changes/#files-a-session-changed)). Under it:
* **Resume in a new tab** opens a tab in the pane’s group, in the folder the session ran in, and types `claude --resume <id>`, `codex resume --no-daemon <id>` or `opencode --session <id>` there. The new tab’s Chat view shows the old conversation, and what you send goes on in the same session.
* **Resume here** types the same line in this pane, offered only when the pane shows its shell and nothing else. As with Tasks, Termote presses Ctrl+C first and types nothing if the pane is busy.
* A session that runs in a pane now is marked **Running**: it is never opened a second time (two processes writing one transcript would break it). **Open the running tab** goes there, and **Fork** starts a new session with its own id from that conversation (`claude --resume <id> --fork-session`, or `codex fork --no-daemon <id>`), leaving the old one as it is. The fork’s Chat view shows the conversation it was forked from, then what is said in the fork. OpenCode’s service makes the fork (a copy of the conversation, titled “… (fork #1)”), which then opens as `opencode --session <fork id>`.
Limits:
* Only sessions of the server’s own user are listed (its `CLAUDE_CONFIG_DIR` or `~/.claude`, and `CODEX_HOME` or `~/.codex`); sessions of another folder or a Place are not.
* Codex keeps no index by folder: only threads among its latest 2000 runs are listed, and the list says so when older ones were left out.
* OpenCode’s sessions come from its background service, only while it runs: its latest 2000 sessions, without a subagent’s. Their tokens and the files they changed are not shown yet.
* Resuming needs a backend that runs [Tasks](/usage/tasks/) (tmux, Herdr, Zellij, `herdr-core`; not psmux) and a shell Tasks knows (`bash`, `zsh`, `sh`, `fish`, `pwsh`).
* View-only devices can read the list and the old conversations, where they may read [Files](/usage/files/) (a git repository, not a home folder), but cannot resume.
## Limitations
[Section titled “Limitations”](#limitations)
* tmux: the chat follows the pane that has focus in the window. In a split window, it talks to the focused pane.
* Herdr before 0.9.3: the transcript is read from `~/.claude` of the user running Termote. A Claude Code started with another `CLAUDE_CONFIG_DIR` shows no history. From Herdr 0.9.3 the config dir of the Claude Code running in the pane is used, as on tmux.
* Herdr: Claude Code reports its session to Herdr through hooks in its own config dir. A Claude Code started with another `CLAUDE_CONFIG_DIR` (a per-account shell function, for example) needs the integration installed there too, or the Chat view says no session was found: `CLAUDE_CONFIG_DIR=~/.claude-work herdr integration install claude`, then restart Claude Code.
* Container mode is not supported.
* Only Claude Code, Codex and OpenCode for now.
Note
**Open terminal** is always one tap away. The terminal stream stays connected while you are in the Chat view, so switching back is instant.
# The Changes View
> Read what git reports as changed in a pane's repository from your phone: the diff of each file, the last commit, staging hunks, review comments and blame.
When a pane’s directory is in a git repository, **Changes** lists the files git reports as changed and shows the diff of each one; for an image, the version before and after. The pane’s files themselves are in [Files](/usage/files/).

## Changes
[Section titled “Changes”](#changes)
The list follows `git status` and is read again every 5 seconds while the view is open and the browser tab is visible. It is grouped:
* **Conflicts**: files with an unresolved merge conflict
* **Staged**: changes added to the index
* **Changes**: changes in the working tree that are not staged
* **Untracked**: new files git does not track yet
Each file shows a letter: `M` modified, `A` added, `D` deleted, `R` renamed (with the old path), `C` copied, `T` type changed, `U` conflict, `?` untracked. A file with both staged and unstaged changes is listed in both groups, and each opens the diff of its own side. An untracked file opens as all added lines. A diff reads the file’s bytes as UTF-8 only: in another encoding its lines show garbled characters, and an untracked or conflicted one shows as a binary file. Its preview (a Markdown file’s current version) is decoded as in Files.
The diff shows each hunk with the old and new line numbers (one column on a narrow phone screen), added lines on green and removed lines on red, with `+` and `-` kept so the colour is never the only sign.
| Shown | Why |
| --------------------------------- | ------------------------------------------------------------------------- |
| `Not a git repository` | The pane’s directory is not inside a git repository |
| `Binary file changed` | git reports a binary change; there are no lines to show |
| `Diff truncated at 1 MiB` | The diff of this file is longer than 1 MiB; only its start is shown |
| `Unresolved conflict` | A conflicted file is shown whole, as it is now in the working tree |
| `No longer changed` | git stopped listing this side of the file while it was being read |
| `git took too long; trying again` | `git status` took over 10 seconds (a very large repository); it backs off |
Submodules are not listed, and their contents are not compared.
Diffs open as tabs, the same way files do in Files (see [Open Files as Tabs](/usage/files/#open-files-as-tabs)), with tabs of their own:
* Each side of a file is a tab: the staged and the unstaged diff of one file are two tabs, the staged one named `(staged)`. The first tab, **Changes**, shows the list.
* A single click opens the preview tab; a double click (on the list or on the tab) or **Edit** keeps it. **Edit** opens the editor in that tab, and switching tabs keeps it with its unsaved changes. Back from the diff shows the list; the tab stays open.
* When git no longer lists a side (committed, staged, reverted), its tab closes with `No changes left in <file>`. A tab being edited stays open.
* Closing, the limit of 10, the sheet on a phone, a move of the root and a file that may contain secrets work as in Files.
* A right click on a row of the list (a long press on a phone, Shift+F10 or the menu key) opens its menu: **Open file** in Files (not for a deleted file), the copies and **Insert @path in terminal** as in Files, then **Stage file**, **Unstage file** or **Discard file** for that side.
When the list has more than 15 entries, a **Filter changes…** box appears above it. It keeps the files whose path contains what you typed, ignoring case (for a rename, the old path counts too). Groups with nothing left are hidden, the counts show the filtered numbers, and `No changes match` shows when nothing is left. Escape clears the box. The filter lives in the page only: it is cleared when you leave the view or when the root moves.
### Files a Session Changed
[Section titled “Files a Session Changed”](#files-a-session-changed)
In a pane that runs Claude Code or Codex, a switch above the list picks between **All changes** and **Changed by this session**. The second lists the files the agent’s session changed through its own tool calls (Claude Code’s Edit, MultiEdit, Write and NotebookEdit, Codex’s file changes), its subagents’ included, the latest first. It is read again every 5 seconds while shown, and the choice is kept for the pane.
* Each file shows its status in git now, with the same letters as the list, or `no longer changed` once it was committed or reverted. Tapping it opens its diff (the unstaged side, or the staged one when that is all there is), or the file in Files when git lists no change of it. A file the session deleted that is gone from git shows `deleted` and opens nothing.
* `×3` says how many times the session changed a file, `subagent` that only its subagents did, `editing` that a change is still waiting for its result. An edit that failed (its text was not found, or you declined it) is not listed.
* Files changed by shell commands (`sed -i`, `mv`, a script) are never in this list, which says so at its top; the list of all changes still has them. Files outside the pane’s root or that Files never shows (`~/.ssh`, the `.git` dir) are left out and only counted.
* A view-only device sees only the files git tracks that it may read, by name, without the counts or labels, and nothing outside a git repository or in a home folder.
An old session in the [session history](/usage/agent-chat/#resuming-an-old-session) lists its files the same way under **Files changed**.
### Last Commit
[Section titled “Last Commit”](#last-commit)
When there is nothing left to list, Changes says `No changes since the last commit` with a **Show last commit** button. It opens the commit `HEAD` points at, read only: its message, its short hash, author and date, and the files it changed, each with the same letters as the list. Tap a file to see its diff; Back returns to the commit, then to Changes.
* A merge commit shows what it brought into the branch: its diff against the first parent.
* A file that may contain secrets asks first, as in the list; a view-only device never shows it.
* When another commit lands while a diff is being opened, the view reads `HEAD` again and shows the new commit.
* A repository with no commit yet shows `No commits yet`.
Only the last commit is shown: there is no history to walk back through.
### Staging, Unstaging and Discarding
[Section titled “Staging, Unstaging and Discarding”](#staging-unstaging-and-discarding)
A row of the list has **Stage**, **Unstage** or **Discard** buttons, and the same ones are in the header of an open diff. On a diff, each hunk has its own, so one hunk can go to the index or be thrown away alone.
* **Stage** adds the file (or the hunk) to the index, **Unstage** takes it out again. Only the index changes; the file in the working tree is never touched.
* **Discard** throws away the change in the working tree and makes the file what the index holds. A dialog asks first and shows the path. A deleted file is made again, and an untracked file is deleted.
* Discard is not final. What it replaces is kept in Termote’s trash (see [Deleting a File](/usage/file-editing/#deleting-a-file) for how long), and a toast with **Undo** puts it back. Undo works only while the file still holds what the discard left: if something (often the agent) changed it since, nothing is overwritten.
* An untracked file is discarded as a delete: on another file system than the trash it asks a second time before deleting it for good.
Some changes are applied as a whole file only, or not at all:
* A hunk can be applied alone only for a change in place of a regular file, with no rename, copy, mode change or binary content. A renamed file is staged and unstaged whole (the old and the new path together).
* A file with a merge conflict cannot be staged, unstaged or discarded here, nor can a symbolic link or a submodule be discarded; use a terminal.
* A file that goes through a git filter (such as Git LFS) cannot be changed here either: the filters are off, so staging it would store the wrong content.
* A changed or deleted file larger than 1 MiB cannot be discarded here (an untracked one can: it is deleted into the trash).
* When the file changed after the diff was read (a hunk is gone or moved), the app refuses and the list is read again. When another git command holds the repository’s index lock, try again in a moment.
* A file that may contain secrets asks for confirmation first, as for its diff.
* A view-only session cannot change anything.
### Review Comments
[Section titled “Review Comments”](#review-comments)
Instead of typing a file name and line numbers into Chat, comment on the lines themselves and send the whole review to the agent at once.
* **Pick lines**: click a line number in a diff, or in a file open in Files. Shift+click another number to extend the range; on a touch screen, tap a second number. Tapping the only line picked again clears it. A range stays inside one hunk of a diff. In the editor, select the lines (or put the cursor on one) and press **Comment on lines** in the header.
* **Comment**: press **Comment** under the code, write the comment and press **Add** (Ctrl/Cmd+Enter, or Enter alone when **Settings → Keyboard** says so). A line with a comment has a mark next to its number.
* **Review**: the **Review** button in the header shows how many comments are waiting. It lists them by file, each with its lines and where they were read (`unstaged diff`, `staged diff`, `file`, or `file (unsaved edits)` from an editor with changes not saved yet). Click one to open its diff or file at that line, or edit or delete it. **Clear** deletes them all.
* **Send**: the review goes as one message: each comment under its file and lines, with the lines quoted as they were when you commented. When the pane runs Claude Code or Codex, it goes to that agent. Otherwise pick a pane running one; Termote remembers the pick for the tabs of that group, the same pick as Tasks’ [Send to agent](/usage/tasks/#send-to-agent).
The comments are cleared only once the agent took the message (or queued it while busy). When the send is refused, they all stay. A comment added while a send is on its way stays too.
| Shown | Why |
| ----------------------------------- | --------------------------------------------------------------------- |
| `No pane runs Claude Code or Codex` | No agent can receive the review; start one in a pane first |
| `Too long for one message: …` | The review is over 16 KiB; delete or shorten comments, nothing is cut |
| `Review is full (50 comments)` | A pane’s review holds up to 50 comments |
| `The pane is in another folder now` | The comment was made under another root than the pane’s now |
Each comment holds up to 4 KiB, and quotes up to 60 lines (the rest is counted). Characters that change or hide text without showing are written out as `⟨U+200B⟩`, as for Tasks.
The review lives in this browser tab only (like an unsent Chat message): it stays across a reload and when you switch tabs, and is gone when the browser tab closes or you sign out. A window of its own (**Open in new window**) and a view-only device have no review comments. A file that may contain secrets can be commented on once shown, but its lines are never quoted, neither in the message nor in what the tab keeps: only the file and line numbers go.
## Blame
[Section titled “Blame”](#blame)
In a file of a git repository, the **Blame** button in the viewer’s header (on a phone, in the **More actions** menu) adds a column before the line numbers: who last changed each line and how long ago, written once for each run of lines from the same commit. Lines not committed yet read `Not committed`. Tap a name to see the commit’s summary, author, date and short hash, with a **Copy hash** button; Escape or a tap elsewhere closes it. Tap **Blame** again to hide the column. It stays on as you open other files, until Termote is reloaded.
* Blame follows the commits reachable from `HEAD`. A file that is not in the last commit yet has nothing to blame, and the viewer says so.
* It is offered for the text in read mode only: not while editing, nor in a Markdown preview, a table or an image.
* A file larger than 1 MiB is not blamed.
* When the file changed on the host since it was opened, the viewer reads it again first, so each name stands beside the line it is about.
* When git counts the lines of the file differently from the viewer (a UTF-16 file), the column is hidden with a note.
* A view-only device sees blame too, for the files it may read.
# Pair a Device
> Sign a phone or a colleague's browser in to Termote with a one-time code from the terminal or a signed-in device, view only or full, and revoke it later.
Everyone signs in with the one saved password. To let another device in without handing that password over, pair it: you make a one-time code, the new device types it, and from then on that device signs in by itself. Each paired device has a name and a role, and you can sign it out on its own without changing the password. Pairing needs sign-in, so it is off with `--no-auth`. What a paired device can and cannot be trusted with is on [Security Model](/usage/security/).
## Pair from the terminal
[Section titled “Pair from the terminal”](#pair-from-the-terminal)
```bash
termote pair # a view-only device (the default), signed in for 30 days
termote pair --role full --name "Work phone"
termote pair --expires 7d # 12h, 7d, 2w: 1 hour to 400 days
termote pair --expires never # no time limit
```
It prints a code like `ABCDE-FGHJK`, a link and a QR code. The code works once and expires after 5 minutes. The device it pairs stays signed in for 30 days unless `--expires` says otherwise; past that it is signed out and has to be paired again. On the new device, scan the QR code, open the link, or open `/pair` on your Termote address and type the code. If you gave no `--name`, the new device is asked for one, else it is named after its browser.
## Pair from a signed-in device
[Section titled “Pair from a signed-in device”](#pair-from-a-signed-in-device)
On a device with full access, open **Settings > Devices > Pair a device**, pick **View only** or full access, how long it **Stays signed in for** (30 days unless you pick another, or **Never**), optionally name it, and show the code or QR code to the new device. The same list shows each paired device with its role, when it was last used, who paired it and until when it is valid, with a **Revoke** button.
Only the password pairs a full device: signed in with it, or `termote pair` on the server. A paired device pairs view-only devices only, and a device with a time limit pairs none that outlasts it (no **Never**).
Note
An iOS home-screen app does not share cookies with Safari, so a scanned QR code cannot sign it in. Open the app’s sign-in page and use **Have a pairing code?**.
## List and revoke
[Section titled “List and revoke”](#list-and-revoke)
```bash
termote devices # id, name, role, paired, last used, valid until, paired by
termote devices revoke <id> # sign it out and close its open terminals
```
Revoking takes effect at once, and also revokes every device the revoked one paired. Logging out on a paired device revokes only that device: the devices it paired stay, listed as paired by a removed device. Signing out a browser in Settings > Signed-in browsers (or `termote signins revoke`) revokes the devices it paired, listed as paired by a signed-in browser. A device a browser paired before 2.0.1 is listed as paired with the password, and stays when that browser signs out: revoke it by hand. A new password (`termote start --fresh`) signs out every paired device. After revoking a full device, also run `termote start --fresh`: it had a shell (see [Security Model](/usage/security/#if-a-device-is-lost)).
A device paired with **Never** stays signed in until you revoke it, so give Termote a host name of its own: a cookie is shared by every service on the same host name.
## View only
[Section titled “View only”](#view-only)
A view-only device can watch the terminal, the Chat view and Files/Changes, and nothing else: the server refuses every write, and the interface hides the controls that would write, under a **View only** banner.
* **Terminal:** it cannot type, resize or scroll the shared view. With tmux it follows each session’s active window; with Herdr it picks tabs on its own device but cannot scroll the history.
* **Needs tmux 3.2 or later:** with an older tmux, or psmux, the terminal is not available to a view-only device and the banner says so. Chat and Files still work.
* **Files:** only for a pane inside a git repository that is not your home dir (or above it), only the files git tracks (never one nobody has `git add`ed yet, nor one git ignores, such as a `.env.local` or a log; a file just added shows up to 30 seconds late), and never a file marked as sensitive (`.env`, keys, and so on). Changes lists only the changes of tracked files.
* **Chat:** it reads what you and the agent wrote and any open dialog, but cannot answer it. Tool calls and command output are left out altogether: no command, file, input or result of any tool, and no output of a `!` command. An open dialog’s card still shows what the terminal shows.
* **Everything the agent printed is readable.** Pair a view-only device only with someone you would let look over your shoulder.
* No push notifications, and at most 2 open terminals per view-only device.
## Container
[Section titled “Container”](#container)
`termote container up` keeps the paired devices in a volume named `termote-state-<uid>`, so they survive `container up` and `update`. `termote container down` keeps it; `termote uninstall --purge` removes it.
# Editing Files
> Edit and save a text file in Termote, create a file, delete a file or an empty folder into the trash, and rename or move a file, from a phone or a desktop.
A text file opened in [Files](/usage/files/) or [Changes](/usage/changes/) can be edited and saved there; a file can also be created, deleted, renamed or moved from Files.
## Editing a File
[Section titled “Editing a File”](#editing-a-file)
The pencil button (**Edit**) in a text file’s bar turns its text into an editor, with **Cancel** and **Save** in place of the other buttons. It is meant for small changes, such as turning `status: review` into `status: approved` in a record’s front matter, without asking the agent or typing `sed`.
On a computer with a mouse the editor shows line numbers and colors the code, and has the keys below. On a phone or a touch tablet the text is in a plain text box, as it always was. If the editor cannot load (the connection dropped), the plain text box takes its place with `Could not load the editor` and **Retry**: what you typed is kept either way.
| Keys | What they do |
| -------------------------------------------------- | ------------------------------------------------------------------- |
| Ctrl+S (Cmd+S on a Mac) | Save |
| Ctrl+F (Cmd+F on a Mac) | Find in the file; Enter or F3 goes to the next match, Escape closes |
| Ctrl+H | Find and replace (Cmd+Alt+F too on a Mac) |
| Ctrl+G | Go to a line |
| Ctrl+D (Cmd+D on a Mac) | Select the next occurrence of the selection |
| Alt+drag, Ctrl+Alt+↑/↓ (Cmd+Alt+↑/↓ on a Mac) | Several cursors |
| Ctrl+Z, Ctrl+Shift+Z (Cmd+Z, Cmd+Shift+Z on a Mac) | Undo, redo |
| Tab, Shift+Tab | Indent, unindent |
Ctrl+H and Ctrl+G are the Ctrl key on a Mac too: Cmd+H hides the browser there, and Cmd+G finds the next match. Tab indents inside the editor; to move the focus out with the keyboard, press Ctrl+M (Shift+Alt+M on a Mac), then Tab. Font size, tab size, line wrapping and Vim keys are in Settings → Files (see [Settings](/usage/settings/#editor)). With Vim keys on, the keys are Vim’s (`/` finds, `:w` saves), except Ctrl+S, which still saves.
A search result opens the editor at the match when the file is being edited, and Edit in Changes opens it at the first change of the diff.
* A Markdown file is edited beside its preview (see [Editing Markdown](#editing-markdown)). A CSV or TSV file shown as a table is edited in the table (see [Editing a Table](/usage/file-previews/#editing-a-table)); shown as its source, or opened from Changes, it is edited as its source.
* Ctrl+S (Cmd+S on a Mac) saves. Cancel, or Back, with unsaved changes asks `Discard changes?` first.
* Line breaks stay as the file had them: a file with Windows line breaks (`\r\n`) keeps them.
* The text being edited stays while you switch views or tabs, or the panel closes and opens again. Each file keeps its own unsaved changes, so several files can be edited at once; a file edited from Changes shares its text with the same file in Files. It is kept only in the page’s memory, never in the browser’s storage, so closing or reloading the app loses it (a phone may close an app left in the background).
* In **Changes**, the diff of a file the working tree still has as text has the same Edit button (**Edit working copy** on the staged side: the working tree’s file is what is saved). After saving, the diff of what is not staged shows, read again; when the file then matches the index, the list shows with `No changes left in <file>`.
When saving does not go through, your text stays in the box:
| Shown | What to do |
| ---------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `The file changed on the host since you opened it` | Something (often the agent) wrote the file meanwhile. **Reload** reads it again and drops your text; **Copy my text** copies it first. A save never overwrites the other change. |
| `This pane's folder changed` | The pane moved to another directory. Copy your text, then Cancel to see the file there. |
| `Not sure the file was saved. Saving again is safe.` | The connection dropped or the server did not answer within 30 seconds. Save again: the same text is not a conflict. |
| `The server can't write this file` | The file is read-only, belongs to another user, or its folder cannot be written |
| `The file system there is read-only` | The file is on a read-only disk or mount, such as a container volume mounted read-only |
| `The host's disk or quota is full` | The host’s disk, or your disk quota, has no room left |
| `Line 3 has "😀", which Shift-JIS can't hold` | The file’s encoding has no such character (see [Encodings](#encodings)). Change it, or save as UTF-8 |
| `Reload the app to save this file` | The text was kept by an older version of Termote, for a file that is no longer UTF-8. Copy your text, reload the app and open the file again |
Some files have no Edit button, because the server would refuse the save: a symbolic link (or a file reached through a linked folder), a file with several hard links, a file that mixes line break styles or holds a NUL character, a file over 1 MiB, an image, a file that would not encode back to its own bytes (see [Encodings](#encodings)), and a file in Termote’s own data or upload folders. A file that may contain secrets can be edited once it has been shown.
Caution
A save replaces the file with a new one in the same folder. If the agent writes the same file in the instant between the last check and the replacement, its change is lost. The new file keeps the old one’s permissions, but not extended attributes or ACLs.
A view-only session (once that role exists) has no Edit button. Until the server enforces roles, this is only hidden in the app.
### Editing Markdown
[Section titled “Editing Markdown”](#editing-markdown)
A Markdown file being edited shows in one of three layouts, picked with the button next to **Save** (in the **More actions** menu on a phone) and remembered on this device:
| Layout | What shows |
| -------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------- |
| Source and preview (the default) | The editor on the left, the file rendered on the right |
| Source | The editor alone |
| Preview | The rendered file alone. The editor is only hidden: undo and the cursor are still there when it comes back, and Ctrl+S still saves |
* Side by side needs a mouse and a Files view at least 768 px wide: widen the side panel, or open the file in a window of its own. Narrower, or on a touch screen, the button switches between the source and the preview, and the layout picked comes back once there is room.
* The preview follows what you type a moment after you stop (a little later in a draft of more than 65,536 characters). A Mermaid diagram keeps its last picture while its source is typed, and is drawn again once the source has not changed for a moment; a source that cannot be drawn yet says why under the diagram’s bar.
* Side by side, the two scroll together: scroll either one, and the same part of the file comes to the top of the other.
* A link of the preview opens the other file; Back returns to the draft, with the editor where it was.
* A draft of more than 262,144 characters shows as its source, with `Too large to preview: shown as source`.
#### Pasting an Image
[Section titled “Pasting an Image”](#pasting-an-image)
Paste an image (a screenshot, a picture copied from another app) into the editor of a Markdown file, and it is saved on the host next to the file: in the file’s `assets` folder when there is one, else in the file’s own folder, named `pasted-<date>-<time>.png` (or `.jpg`, `.gif`, `.webp`). `![Uploading …]()` stands at the cursor while it is sent, then turns into a link to the image, relative to the file, such as ``. Undo takes the link out; the image stays on the host.
* PNG, JPEG, GIF and WebP, up to 10 MB and 40 megapixels. HEIC (an iPhone photo) is refused: share the photo as JPEG.
* Copying from a spreadsheet or a web page puts text on the clipboard too: that pastes as text.
* No folder is made and no file is replaced: when the name is taken, the image gets `-2`, `-3` and so on.
* Several images pasted in a row are sent one after another.
* When the draft is discarded, or the placeholder deleted, before the image arrives, no link is added: a message says where the image was saved. A save meanwhile writes the placeholder into the file, and the message says so.
* A folder that may hold secrets (`.ssh` and the like) takes an image only when the file being edited was shown on purpose.
* The preview never loads an image: it shows as a link, `image: <name>`, that opens it in Files.
### Encodings
[Section titled “Encodings”](#encodings)
The second line of a text file’s bar names its encoding after its size, such as `12.0 KiB · Shift-JIS`. Files reads and saves:
* UTF-8, with or without a byte order mark (`UTF-8 with BOM`);
* UTF-16 LE and UTF-16 BE with a byte order mark;
* Shift-JIS and EUC-JP, recognised when their bytes are valid and enough of the text is Japanese (kana and kanji).
A save writes the file back in its own encoding, with its byte order mark and its line breaks: the bytes you did not edit stay exactly as they were.
* Tap the encoding to read the file again as another one (**Reopen with encoding**). This is how a UTF-16 file without a byte order mark, or a short Shift-JIS file without kana, opens: it shows as `Not previewable`, with **Open with encoding…** under its name. Reopening is off while the text being edited has unsaved changes (`Save or discard your changes first`). The choice lasts for that tab only: a file opened in a new window, or opened again later, has its encoding found again.
* **Save as UTF-8**, in the same menu for a file not in plain UTF-8, asks first, then writes the file as UTF-8 without a byte order mark. Tools that read it in the old encoding then show its text wrongly.
* A few Shift-JIS files decode but would not encode back to the same bytes: characters of the IBM extension area, such as 髙 and 﨑, have two codes. Such a file has no Edit button, and the menu says `Can't be edited in this encoding`; save it as UTF-8 to edit it.
* A file with bytes that are not valid in its encoding (a damaged file, or one read as the wrong encoding) shows them as �. It cannot be edited, nor saved as UTF-8, since those bytes would be lost: the menu says `Can't be edited: some bytes are not valid in this encoding`. Try another encoding.
* In Shift-JIS and EUC-JP, characters an iPhone, iPad or Mac types that the file holds in another form are saved in that form: 〜 → ~, − → -, ‖ → ∥, ¢ → ¢, £ → £, ¬ → ¬, — → ―, ¥ → ¥. Any other character the encoding cannot hold, such as an emoji, stops the save with its line: `Line 3 has "😀", which Shift-JIS can't hold. Change it or save as UTF-8`. It is never replaced by `?`.
* Other encodings (Windows-1252, Latin-1, GBK, Big5, ISO-2022-JP and so on) are not read: such a file shows as `Not previewable`. A short one can pass for Shift-JIS by chance; Reopen with encoding shows it as another one, and saving cannot damage it unnoticed, since the save writes back exactly the bytes you did not edit.
## Creating a File
[Section titled “Creating a File”](#creating-a-file)
The **New file** button (a page with a plus) at the top of Files asks for a path from the pane’s directory, such as `docs/notes/today.md`. It starts with the folder you last tapped in the tree. **Create** makes an empty file, and every folder on the path that is missing, then opens it in the text box to type its contents and **Save**, in a tab of its own. The button shows over the tree only, not while a file is shown.
* An existing file or folder is never replaced: the box says `A file or directory of that name already exists`, with **Open it** to open that one instead.
* A name that usually holds secrets (`.env.local`, for example) asks `This name usually holds secrets. Create it anyway?` once; the new file then opens without asking to show it.
* When the connection drops before the server answers, the box says `The file may have been created. Refresh to check.`, and **Refresh** reads the tree again.
**New folder**, in a folder’s menu or the tree’s (see [Right-Click Menus](/usage/files/#right-click-menus)), asks the same way and makes an empty folder, which then shows open in the tree. The same names are refused, and the same answers given, as for a file.
* Closing the box while **Create** runs does not stop it: the file is still made and shows in the tree, but it does not open.
* The folders made for the path stay when a later step fails, as with `mkdir -p`. A new file left empty stays too.
| Shown | Why |
| ---------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `Part of this path is a file, not a directory` | A folder on the path is a file |
| `Part of this path is a symbolic link` | A folder on the path is a symbolic link: nothing is ever created through one |
| `Files can't be created there` | A path Termote never serves (see [Never Shown](/usage/security/#never-shown)), or its own data or upload folders |
| `The server can't create a file there` | The folder cannot be written |
| `The file system there is read-only` | The folder is on a read-only disk or mount, such as a container volume mounted read-only |
| `The host's disk or quota is full` | The host’s disk, or your disk quota, has no room left |
| `This name can't be used` | An empty part (`a//b`, a trailing `/`), a part that ends in a dot or a space, a control character such as a line break, more than 32 levels or 1024 bytes, or on Windows one of `<>:"\|?*` |
Caution
A new file can be one that another tool reads or runs by itself, such as `.claude/settings.local.json`, `.vscode/tasks.json` or a file under `.github/workflows`. Anyone who can sign in to Termote can already run commands in the terminal, so this allows nothing new, but check what you create in such places.
A view-only session (once that role exists) has no New file button either; until the server enforces roles, it is only hidden in the app.
## Deleting a File
[Section titled “Deleting a File”](#deleting-a-file)
A file or an **empty** folder can be deleted from Files, three ways: **Delete** in the **⋯** menu of a row in the tree (with a mouse the button shows on hover or focus; on a touch screen it is always there), the Delete key on a focused row, or the **Delete** button in the header of an open file (not while it is being edited). A dialog shows the full path and asks first; **Cancel** has the focus, so pressing Enter does not delete.
* A deleted file is not erased: it moves to Termote’s trash on the host. A toast says `Deleted <name>` with **Undo** for 10 seconds, which puts it back where it was (it never replaces anything now there). A deleted empty folder is made again by Undo.
* The trash keeps a file for 7 days, at most 1 GiB in all (the oldest go first), and never removes one younger than an hour. After the toast is gone, open the [Trash](#the-trash) to get it back.
* A folder that is not empty is not deleted, and neither is the root itself.
* Deleting asks the server to check that the file is still the one you saw. If something (often the agent) changed it meanwhile, nothing is deleted.
* On another file system than the trash’s (WSL’s `/mnt/c`, a mounted volume, the container’s `/workspace`), the file cannot be moved to the trash. The dialog then asks a second time, `Delete permanently? This cannot be undone.`, and only after that deletes it, with no Undo. In the container, every delete under `/workspace` is permanent in this way.
* A file whose name usually holds secrets is marked `This is a sensitive file` in the dialog.
| Shown | Why |
| ------------------------------------------------------------- | ------------------------------------------------------------------- |
| `The folder is not empty` | Only an empty folder is deleted |
| `The file changed since it was read` | Something changed the file after the dialog read it |
| `Part of this path is a symbolic link` | Nothing is deleted through a link, and a link itself is not deleted |
| `This file has several hard links; delete it from a terminal` | Another name would keep the file |
| `The server can't delete this` | The file belongs to another user |
| `The file system there is read-only` | The file is on a read-only disk or mount |
| `Too large to delete here; use the terminal` | The file is over 512 MiB |
| `Deleting is not available on this server` | The server has no usable trash |
A view-only session (once that role exists) has no Delete button; until the server enforces roles, it is only hidden in the app.
Note
`termote uninstall` keeps the trash and prints its path; `termote uninstall --purge` removes it too. See [Uninstall](/installation/native/#uninstall).
### The Trash
[Section titled “The Trash”](#the-trash)
The **Trash** button in the Files header (a full session on a server with a trash, also with a file open and in a Place) opens a sheet listing what was deleted under the folder shown, newest first: the name and its folder, whether it was a file, a folder or a discarded change in [Changes](/usage/changes/#staging-unstaging-and-discarding), when, its size and `Removed by <date> at the latest`. The list is read each time the sheet opens.
* **Restore** puts an entry back where it was, never replacing anything there. A sensitive file asks first. A discarded change puts the discarded text back over the file, only while the file is still what the discard left; an open tab of the file and Changes read it again.
* **Delete permanently** removes one entry for good, after asking. This works for something deleted a moment ago too.
* **Empty trash** removes the entries the sheet shows, after asking, and no other: something deleted meanwhile, on this device or another one, stays (and its Undo still works). When more than 500 entries are kept, only the 500 newest show and are emptied; the list is read again and the older ones show next.
* An entry already restored or removed elsewhere leaves the list with `Already restored or removed`.
* Only a full session sees the Trash: a view-only one has no button.
Caution
The Trash shows only what was deleted while the folder shown was the root. Something deleted from another folder, before a `cd` into a subfolder or from a Place or repository that holds this folder, is in that folder’s Trash: open it there. A file on another file system than the trash is deleted for good, so it is never in the Trash.
## Renaming or Moving a File
[Section titled “Renaming or Moving a File”](#renaming-or-moving-a-file)
A file can be renamed from Files: **Rename** in the **⋯** menu of its row in the tree, or the **Rename** button in the header of an open file (not while it is being edited; a file with unsaved changes is renamed from its row). On a phone, a finger resting on a row opens the same menu. The dialog shows the path from the pane’s directory with the name, without its extension, selected.
* Change the folders in the path to move the file. A folder that does not exist yet is created.
* Nothing is ever replaced: if a file or folder of the new name exists, the dialog says so and offers **Open it**.
* The file’s open tab, its unsaved changes and its editor in Changes follow it to the new path, in every pane showing the same directory. Saving afterwards writes the new path.
* A toast says `Renamed <old> to <new>` (or `Moved <name> to <folder>`) with **Undo**, which renames it back. Folders made for the move stay. A file whose old name the server would not accept as a new one (one ending in a dot or a space) has no Undo.
* Changes then shows a deleted file and an untracked one, as after `mv`, until you stage them; Termote does not run `git mv`.
* Only files are renamed, not folders or links.
* A file whose name usually holds secrets, before or after, is renamed after a second ask.
| Shown | Why |
| -------------------------------------------------------- | -------------------------------------------------------------------------- |
| `A file or directory of that name already exists` | The new path is taken; nothing was replaced |
| `This file no longer exists` | Something removed or moved the file after the tree was read |
| `<path> has unsaved changes. Save or discard them first` | The file at the new path is being edited |
| `The file changed while it was being renamed` | Something replaced the file during the rename; nothing was moved |
| `Can't move a file to another file system` | The new folder is on another disk or mount; a file is never copied |
| `The file is open in another program on the host` | Windows: another program holds the file open |
| `Part of this path is a symbolic link` | Nothing is renamed through a link |
| `The server can't rename this file` | The file or its folder belongs to another user, or the folder is protected |
| `The file system there is read-only` | The file is on a read-only disk or mount |
A view-only session has no Rename.
# File Previews
> Preview a pane's files in Termote: Markdown with diagrams, CSV and TSV as a table you can search and edit, images, and PDF, Excel and Word documents.
Besides text, [Files](/usage/files/) shows some kinds of file as what they are: Markdown as a page, CSV and TSV as a table, an image as the picture, and PDF, Excel and Word documents as a read-only preview.
## Markdown Preview
[Section titled “Markdown Preview”](#markdown-preview)
A Markdown file (`.md`, `.markdown`, `.mdx`) opens rendered: headings, lists, tables (they scroll sideways on a phone), task lists, footnotes and code. The eye button in its bar switches between the preview and the source; the choice is remembered on this device. A Markdown file over 256 KiB opens as its source, with a note that it is too large to preview.
* **Code blocks** are highlighted in the language named after the opening fence (` ```ts `, ` ```bash `, ` ```go `…), with a button to copy the block. A block in a language not covered is shown as plain text. The wrap button wraps code blocks too.
* **Mermaid diagrams** (` ```mermaid `) are drawn as images once you scroll to them: flowcharts, sequence, class, state, Gantt and the other kinds. The code button in the block’s bar switches between the diagram and its source; Copy always copies the source. A wide diagram scrolls sideways. The diagram is a picture: nodes and links in it cannot be clicked, and the diagram follows the light or dark theme of Termote, whatever its `%%{init}%%` line or front matter asks for. A shape with an image or an icon (`@{ img: ... }`) is drawn without it, so nothing is loaded. The source is shown instead, with the reason, when the diagram is too large (over 50 KB, or over 500 links), is not valid Mermaid, is of a kind the renderer does not know, takes longer than 10 seconds, or the renderer cannot be loaded. The renderer is downloaded the first time a diagram is shown, not when the app is installed: offline, until then, diagrams show as their source with “Could not load the diagram renderer”. Diagrams are drawn in the background, so even a large one never makes the page stop responding.
* **Saving a diagram.** Once drawn, a diagram’s bar has three more buttons, also in its full screen view: **Share** opens the share sheet with the diagram as a PNG (on an iPad or iPhone: Save Image, Notes, Files or any note app), **Copy image** puts the PNG on the clipboard to paste into a note, and **Download SVG** saves the diagram as an SVG file. The PNG is twice the diagram’s size (less for a very large one), in the theme it is shown in, with that theme’s background; the SVG file keeps that background too. Named after the file and the diagram’s number, as `README-mermaid-1.png`. Share and Copy image show only where the browser offers them (Firefox on a desktop and Chrome on Linux have no Share), and only on a secure page: over HTTPS (such as `termote start --tailscale`) or on `localhost`. Opened over plain HTTP on the LAN, only Download SVG shows. In the app added to the Home Screen on iOS, Download SVG goes through the share sheet (Save to Files). An SVG pasted into a note app usually shows as text: use the PNG there.
* **Front matter** (the `---` block at the top of docs and MDX pages) is shown as a code block.
* **Raw HTML and MDX components** are shown as the text they are, never run. The one exception is a `<details>` block with its `<summary>`: it shows as a section you tap to expand, and the Markdown inside it renders as usual. Only its `open` attribute is kept, and a `<details>` without its `</details>` stays text.
* **Images are never loaded.** An image inside the root shows as a link that opens the file in Files; an image on the web shows as a link to its site.
Links:
* A link to another file under the root (`docs/guide.md`, `../README.md`, `/src/main.go`) opens it in the same tab; `guide.md#install` opens it at that heading. A leading `/` means the root. A file already open in another tab shows that tab.
* Ctrl+click (Cmd+click on a Mac), a middle click, or a long press on a phone opens the link in a new tab.
* A link to a folder shows the tree with that folder open.
* `#heading` scrolls to the heading in the same file.
* The back button returns to the file the link was followed from in that tab, where you left it, before going back to the tree.
* A link to a missing file says `Not found`. A link that leaves the root, or that uses another scheme than `http`/`https` (`javascript:`, `file:`, `data:`), is shown as plain text.
* `http`/`https` links open in a new tab; hovering shows the site they go to.
In **Changes**, a changed Markdown file has the same eye button over its diff: it shows the current version of the file, rendered with the same rules. A deleted file has no preview. Its links open in Files.
## CSV and TSV as a Table
[Section titled “CSV and TSV as a Table”](#csv-and-tsv-as-a-table)
A `.csv`, `.tsv` or `.tab` file opens as a table: the header row stays at the top and the row numbers at the left while the rest scrolls, and a 1 MiB file scrolls as smoothly as a short one. The eye button in its bar switches between the table and the source; the choice is remembered on this device, apart from the one for Markdown. Changes shows the diff of a CSV file as text first; its eye button shows the current version of the file as a table, to read only (Edit opens it for changes). A deleted file has no table.
* **Delimiter**: a `.tsv` or `.tab` file is read with tabs. For a `.csv` file the delimiter is guessed from its first lines (comma, semicolon, tab or pipe); the **Delimiter** menu reads it again with another one.
* **Quotes** follow RFC 4180: a value in double quotes can hold the delimiter, a line break and `""` for one quote. A value that holds a line break shows `↵` in its cell.
* **Header row** (on at first) takes the first row as the column names. Turned off, the columns are `Column 1`, `Column 2`… and the first row is a row like the others.
* **Sorting**: press a column name to sort by it, again to reverse, a third time to go back to the file’s order. Text sorts without regard to case and runs of digits by their value (`item 2` before `item 10`; decimals are compared digit run by digit run, so `1.5` comes after `1.25`). Empty cells go last, and the header row is never sorted.
* **Filter rows** keeps the rows where any cell contains what you typed, ignoring case, and shows how many are left (`12 / 340 rows`). It searches hidden columns too: while any column is hidden, the count says so (`12 / 340 rows · 2 columns hidden`).
* **Column widths**: drag the line at the right edge of a column name, with a mouse or a finger, to make that column wider or narrower; a drag never sorts. Double-click or double-tap the line to fit the column to its values. From the keyboard, Tab to the line, then Left and Right change the width 2 characters at a time, Home and End go to the narrowest and widest, and Enter fits. A column is 4 to 120 characters wide.
* The **Columns** button in the bar has **Fit columns**, which fits every column to the rows on screen (up to 1000, from the first one in view), **Reset widths**, back to the widths the table opened with, **Wrap**, a list of the columns, and **Lines**: 3, 6 or 10 (3 at first). A wrapped column shows up to that many lines of each value (a line break still ends with `↵`) and its name has a wrap icon; while any column wraps, every row is that many lines tall, and a longer value is cut. The whole value is in its cell’s sheet, or its field while editing.
* Each column name has a ⋯ button (shown on hover, or always on a touch screen); right-clicking a name opens the same menu. **Pin column** keeps that column at the left, next to the row numbers, its name too: it stays in its place until the table scrolls sideways to it, then the columns before it pass under it. One column is pinned at a time; **Unpin column** in the same menu unpins it, and so does **Unpin name** in the **Columns** menu, there while a column is pinned. While the row numbers and the pinned column take more than half the table’s width (a phone, a very wide column), it scrolls with the others, so the table can still be read. **Hide column** in the ⋯ menu hides that column, as **Show** below does; a pinned column hidden is unpinned.
* **Show** in the **Columns** menu lists the columns, each ticked while it is shown: untick one to hide it from the table and from Records, which also says how many are hidden. **Show all** brings them back. The last column shown cannot be hidden, and hiding the column the table is sorted by goes back to the file’s order. Hidden columns are skipped by the arrow keys, Home and End, and Tab in a cell, and **Fit columns** fits only the ones shown.
* **Records** shows one row at a time as `column: value`, with **Previous** and **Next**, for a phone where a row is wider than the screen. Press or tap a row number to open that row in Records; turned off, Records goes back to the table where you left it.
* Tap a cell (or press Enter on it) to see its whole value, with a button to copy it. A cell shows at most 1024 characters; the full value is only in that sheet. With a mouse, a click only selects the cell, as in a spreadsheet, and a double-click opens it: in the sheet, or on a file you can edit in the cell itself (see below); a value opened from Records still shows in its sheet.
* A row with another number of cells than the header has its number in orange; hovering or a screen reader says how many cells it has.
* From the keyboard, the arrow keys, Page Up and Page Down move between cells; Home and End go to the first and last column of the row, and Ctrl+Home and Ctrl+End (Cmd on a Mac) to the first and last cell of the table, in the order shown.
Sorting, filtering, the delimiter, the header row, column widths, wrapped columns, the pinned column and hidden columns only change what is shown. Nothing is written to the file, and they are forgotten when the file is closed. Widths, wrapped columns and their lines, the pinned column and hidden columns also stay while you show another tab and come back, and go back to the defaults when the delimiter or the header row changes.
| Shown | Why |
| --------------------------------------------- | ---------------------------------------------------------------------- |
| `Unclosed quote on line N: shown as source` | A quote opened on that line never closes; the file shows as its source |
| `Showing 200 of N columns. See all in Source` | Only the first 200 columns are shown; the source has them all |
| `No rows` | The file is empty |
Caution
A value is shown as text, never run: a formula such as `=HYPERLINK(...)` shows as typed. Characters that could make a value read as something else, such as the right-to-left override that makes `0001` read `1000`, control characters and zero-width spaces, show as marks like `⟨U+202E⟩`. **Copy** in a cell’s sheet copies the value itself, those characters included, without the quotes that wrap it in the file.
### Finding in a Table
[Section titled “Finding in a Table”](#finding-in-a-table)
Ctrl+F (Cmd+F on a Mac) on a table, whether you are only viewing it or editing it, opens a find box under its bar; what you typed last is selected, ready to type over. Unlike **Filter rows**, it hides no row: every cell that matches is tinted, the current one outlined, and the box counts them (`3 of 17`). Typed in the filter, a cell’s field or a sheet, Ctrl+F is still the browser’s own.
* Enter and Shift+Enter in the box, or F3 and Shift+F3 anywhere in the table, go to the next or previous match, round from the last to the first, scrolling it into view. From the box the focus stays in it; from a cell the focus goes to the match. F3 with the box closed opens it again on the next match. In a cell’s field, F3 sets the value first, as leaving the cell does.
* Matches go row by row in the order shown (after sorting and filtering), then along the columns shown. The header row and hidden columns are not searched; while any column is hidden, the box says so.
* The search ignores case and matches part of a value. **Aa** matches case, the boxed **ab** matches only a whole value, and **.\*** reads what you type as a regular expression (JavaScript, Unicode); one that does not compile shows `Invalid pattern`. Values are searched whole, past the 1024 characters a cell shows. Characters that hide or reorder text, typed in the box, show below it as `⟨U+XXXX⟩`.
* An edit, a sort, a filter or a hidden column finds again: the current match stays on its cell while that still matches, else it moves to the next one.
* Escape or the close button closes the box and puts the focus back on the current match.
A regular expression, and any search of a file over 256 KiB, runs apart from the page, so the table never freezes; one still running after 10 seconds (30 for the first search, while that part loads) is stopped and the box says `Search took too long`; one too deep for the browser says `Search failed`. At most 100,000 matches are counted (`100000+`).
### Selecting, Copying and Pasting
[Section titled “Selecting, Copying and Pasting”](#selecting-copying-and-pasting)
With a mouse and a keyboard, Shift with the arrow keys, Home, End, Page Up or Page Down (and Ctrl+Home or Ctrl+End) grows a range of cells from the cell in focus; Shift+click or a drag from one cell to another selects the cells between, and a drag near the top or bottom of the table scrolls it. An arrow key alone, a click or Escape drops the range. On a phone nothing is selected.
* Ctrl+C (Cmd+C on a Mac), while only viewing too, copies the range, or the cell in focus, or from a row number its row, as a spreadsheet does: tab separated, one line a row, in the order shown, hidden columns left out. Values are copied whole, past the 1024 characters a cell shows; one that holds a tab, a line break or a quote is put in quotes, so it pastes into a single cell. At most 100,000 cells are copied at once.
* Ctrl+V while editing pastes a block copied from a spreadsheet or another table, from the cell in focus (or the range’s top-left cell) along the rows and columns shown, as one change that one Ctrl+Z takes back; a cell given its own value is left as it is. A block wider than the columns left is refused. A longer one, while the table is neither sorted nor filtered, asks to add the rows it needs at the end; sorted or filtered, it is refused, saying why. Saving changes only the cells pasted.
* A range the block fits into a whole number of times, both down and across, gets the block over and over, as in a spreadsheet: one copied cell fills every cell of the range, a copied row fills every row of a range as wide, and a short line’s missing cells are emptied. Any other range, or a row number with no range selected, takes the block once, from its top-left cell. The 100,000 cell limit counts the cells of the range filled.
* Delete or Backspace while editing clears every cell of the range, along the rows and columns shown, as one change that one Ctrl+Z takes back. At most 100,000 cells are cleared at once.
* No clipboard permission is asked, so this works over plain HTTP on the LAN too, and on a Mac with no text selected. Pasted in a cell’s field, the filter or the find box, the text goes into that box.
### Editing a Table
[Section titled “Editing a Table”](#editing-a-table)
**Edit** on a file shown as a table keeps the table and makes each cell editable, with **Cancel** and **Save** in the bar as for any file. Opened from Changes, or shown as its source, the file is edited as text instead.
* Tap a cell to change its value in a sheet: the value can hold several lines, and characters that hide or reorder text show below it as `⟨U+XXXX⟩`. **Apply** puts it in the table; a cell a short row does not have can be set too.
* With a mouse, a double-click on a cell, or F2 or Enter on the cell in focus, edits the value in the cell itself, the field growing over the rows below to show up to 20 lines (past them it scrolls), at least 12rem wide; a single click only selects the cell, so Ctrl+V on it pastes a block (see above), and a tap on a touch screen, a laptop’s too, still opens it. A double-click while only viewing a file you can edit starts editing first. Enter sets it, Shift+Enter or Alt+Enter breaks the line, and Escape drops what you typed: the table stays in editing, **Cancel** leaves it. Tab and Shift+Tab set it and go to the next or previous cell, from the end of a row onto the next one. Clicking elsewhere, or scrolling the cell out of view, sets it too. The ⤢ button in the cell takes what you typed to the cell’s sheet, for more room and for **Add row below** and **Delete row**. A value longer than 1024 characters, or one that holds characters which hide or reorder text, opens in the sheet.
* With a keyboard, typing on the cell in focus starts editing it with what you type, which replaces its value (Escape keeps the old one); on a file you are only viewing, it starts editing first. A value the field cannot take opens in the sheet with what you typed. Delete or Backspace clears the cell, and one Ctrl+Z brings the value back. With an input method (IME), the first key opens the field: if the first character you composed does not show in it, type it again there. On a phone, typing on a cell does nothing.
* **Insert row above** and **Add row below** (in that sheet) and the **Add a row at the end** button add an empty row and open its first cell. **Delete row** asks first, and says when the row is the file’s first one, which is the header when **Header row** is on. To delete the header, turn **Header row** off first: the header row is then a row you can open.
* While editing, the ⋯ menu of a column name also has the edits below, and right-clicking a cell, or pressing Shift+F10 or the context menu key on a name or a cell, opens the same menu. **Insert column left** and **Insert column right** add an empty column: a delimiter in each row long enough to reach it, nothing else; a shorter row and a blank line stay as they are. With **Header row** on, the new column’s name is asked for at once (Escape leaves it empty). **Delete column** asks first, saying how many cells with a value go with it; the last column cannot go. A row left with one empty cell keeps it as `""`, so it does not become a blank line. Opened from a cell, the menu also has **Insert row above** and **Insert row below**.
* **Rename column** in that menu, F2 on a column name, or a double-click on it (the first click still sorts) edits the name in place, with **Header row** on; on a touch screen, or for a name the field cannot take, it opens in the sheet, which adds or deletes no row around the header. Widths, wrapped and hidden columns and the sort follow their columns when one is added or deleted, undone, or dropped with **Cancel**; a deleted column brought back by Undo has its values, not its width or wrap.
* **Undo** and **Redo**, or Ctrl+Z and Ctrl+Shift+Z (Cmd on a Mac) on the table, walk through your changes; Ctrl+S saves. Typed in the sheet, a cell or the filter, Ctrl+Z and Ctrl+Shift+Z belong to that box; Ctrl+S in a cell sets it, then saves.
* Saving changes only the characters of the cells you set: a value that needs quotes (it holds the delimiter, a quote, a line break, or a space at either end) gets them, a cell that had quotes keeps them, and a new row or column uses the file’s delimiter and line breaks. Every other byte stays as it was.
* The delimiter cannot change while editing. When the one you are viewing with is not the one found in the file, Edit asks first: edits written with another delimiter can shift the columns.
* Sorting and filtering still work while editing; they never change the file.
If the file changed on the host since you opened it, Save says so and writes nothing. **Reload and reapply** reads the file again and applies each of your changes to the row it was made on, found by that row’s values rather than its number, so a row an agent added or removed above it does not matter. A change whose row is gone, or matches more than one row, is listed with why, and a cell’s value can be copied to set it again by hand; changes after an added or deleted row that could not be applied are listed too, since they may concern that row. A column added or deleted is found again by its name in the header row, wherever that column moved, and with **Header row** off at its place while the file has as many columns as before; when its column is gone, or two columns have that name, it is listed, and so is every change after it. A pasted block finds each of its rows the same way and is applied whole or not at all, so one that set a row matching another (two empty rows, say) is listed; rows it added go at the new end of the file. When the file read again is no longer a table that can be edited, nothing is applied and Save stays off. Nothing is saved until you press Save.
## Images
[Section titled “Images”](#images)
A PNG, JPEG, GIF or WebP file (`.png`, `.jpg`, `.jpeg`, `.gif`, `.webp`) opens as the picture, fit to the width of the view, over a checkerboard so transparent parts show. Under it are its size in pixels and its file size. The server checks the file’s contents, not only its name: a file named `.png` that holds something else is not shown.
An SVG file opens as its source first, like any text file. The **Image** button in its bar switches between the source and the picture; the choice is remembered on this device. In Changes, the same button switches between the diff and the pictures.
**Full screen.** Tap or click an image (in Files, either side in Changes) or a Mermaid diagram of a Markdown preview to open it full screen:
* On a phone: pinch to zoom, drag with one finger, double tap to zoom in (and again to fit). The page behind does not move.
* On a desktop: the mouse wheel (or a trackpad pinch) zooms around the pointer, dragging moves the picture, and the buttons or the keys `+`, `-` and `0` zoom in, zoom out and fit.
* Zoom goes from half the fitted size to 8 times it. A diagram or an SVG is drawn again at the new size once you stop, so it stays sharp.
* The back gesture or button, the close button and Escape close it, leaving the file where it was.
* A diagram has its Share, Copy image and Download SVG buttons in a bar at the bottom; what they did shows there for a few seconds.
In **Changes**, a changed image shows the version before and after, side by side on a desktop and one above the other on a phone:
| Side | Before | After |
| -------------------- | --------- | ---------------- |
| Not staged (Changes) | the index | the working tree |
| Staged | `HEAD` | the index |
A new image shows only the after side (`Added`), a deleted one only the before side (`Deleted`). A renamed image reads the before side from its old path, shown next to the label. For a conflict, only the file as it is now in the working tree is shown. The pictures are read again when you press Refresh or when the file’s status changes, not every 5 seconds.
| Shown | Why |
| ----------------------------------------------------------- | ---------------------------------------------------------------------------- |
| `Larger than 10 MiB` | Images over 10 MiB are not shown |
| `Too many pixels to show (over 40 megapixels)` | An image (or a GIF frame) over 40 million pixels, which a phone may not hold |
| `Not previewable (not a PNG, JPEG, GIF, WebP or SVG image)` | The contents are not one of these types (or an SVG without an `<svg>`) |
| `Stored in Git LFS: only the pointer is in git` | git holds a Git LFS pointer, not the image; termote does not fetch it |
| `The image could not be decoded` | The browser could not read the file (damaged, or a format it lacks) |
| `Busy, try again` | Several images are being read at once; the **Retry** button reads it again |
A name that usually holds secrets asks first, as for text. An SVG opened by its address never runs a script: the server sends it with a policy that forbids scripts, and as a download.
## PDF, Excel and Word Documents
[Section titled “PDF, Excel and Word Documents”](#pdf-excel-and-word-documents)
A `.pdf`, `.xlsx` or `.docx` file opens as a read-only preview. Nothing can be edited in it; **Download** saves the whole file to open it in its own app. The file is read and taken apart in a background worker of the browser, so a large file does not freeze the app, and a file that takes too long is given up on with the reason and Download.
* **PDF**: the pages scroll one after another and are drawn as they come into view. The bar above shows the page you are on, the zoom with **−** and **+**, **Fit width**, and **Find** (Enter and Shift+Enter go to the next and previous match, **Aa** matches case). Text can be selected and copied. A link to a web page opens in a new tab; a link inside the document goes to its page. Forms, scripts and attachments in the PDF are not run.
* **Excel**: each sheet shown in Excel has a tab (the arrow keys move between tabs); hidden sheets have none, and a note counts them. A sheet opens in the same table as a CSV file, with filter, sort, find and records, but without editing. A cell shows its value as Excel shows it: dates and percentages formatted, a formula as its value when the file was last saved (a formula never calculated by Excel shows empty), an error as `#DIV/0!`. A merged cell has its value in its first cell. A sheet whose text is over 1 MiB, or wider than 200 columns, shows its first rows or columns, with a note and Download.
* **Word**: the text with its headings, bold, italic, lists, tables (merged cells included), links to web pages and images. Text boxes, columns, headers, footers, comments and tracked changes are not shown. Links inside the document and footnote marks show as plain text. Find with the browser’s own find (Ctrl+F or the browser menu).
On a phone the same views show, with buttons to zoom a PDF (no pinch).
| Shown | Why |
| -------------------------------------------------------- | ----------------------------------------------------------------------------------- |
| `Larger than 30 MiB (…)` | Documents over 30 MiB are not previewed; the size is shown |
| `Password-protected or encrypted file` | An Office file saved with a password (a PDF with one says `Password-protected PDF`) |
| `Not previewable (not a valid PDF, .xlsx or .docx file)` | The contents do not match the name |
| `The file could not be read (damaged or not supported)` | The file is damaged, or uses something the preview cannot read |
| `Too large to preview once unpacked` | An Excel or Word file that says it unpacks to more than 200 MiB |
| `Took too long to open` | The preview gave up; **Retry** reads it again |
| `The download was cut off` | The connection dropped while the file was read; **Retry** reads it again |
Older formats (`.xls`, `.doc`, `.ppt`, `.pptx`) are not previewed: they show as binary files, with Download. Changes shows documents as binary too. A document with a name that usually holds secrets asks first, as for text. A view-only session sees the preview, without Download; each read of a document is written to the server’s log.
# The Files View
> Browse the files of a pane's directory from your phone: Places, right-click menus, tabs, find a file by name or by content, download and upload.
To see what an agent changed, you would otherwise type `ls`, `cat` and `git diff` on a phone keyboard and read the output in a terminal. Termote has two views for this:
* **Files**: the pane’s directory as a tree, opened one folder at a time. A text file opens with line numbers and syntax highlighting, a CSV or TSV file as a table, an image as the picture, and a PDF, Excel or Word document as a read-only preview.
* **Changes**: when the directory is in a git repository, the files git reports as changed, and the diff of each one (for an image, the version before and after).
A file can be found by name in Files (see [Finding a File](#finding-a-file)). A text file can be edited and saved from either view (see [Editing a File](/usage/file-editing/#editing-a-file)), a new one created in Files (see [Creating a File](/usage/file-editing/#creating-a-file)), a file or an empty folder deleted there (see [Deleting a File](/usage/file-editing/#deleting-a-file)), and a file renamed or moved there (see [Renaming or Moving a File](/usage/file-editing/#renaming-or-moving-a-file)). Changes can also stage and discard ([Staging, Unstaging and Discarding](/usage/changes/#staging-unstaging-and-discarding)). Nothing else changes from them: no folder is renamed, and nothing is committed.

## Where to Find Them
[Section titled “Where to Find Them”](#where-to-find-them)
* **Desktop**: the **Files** and **Changes** buttons in the header open a panel to the right of the terminal. The terminal stays on screen and keeps working. One panel shows at a time; press its button again to close it.
* Drag the panel’s left edge to make it wider or narrower (it always leaves room for the terminal); double-click the edge to go back to the default width. With the keyboard, focus the edge and use the Left/Right arrows, Home and End. The width is remembered on this device. The terminal is resized once, when you let go.
* The **Maximize panel** button at the top of the panel lets it cover the terminal or the Chat view for a while; press it again, or Escape, to put it back. The terminal keeps its size underneath, so other devices on the same window see no change.
* **Phone**: Files and Changes are views in the header’s view menu, alongside Terminal (and Chat, when Claude Code runs in the pane). The button shows the current view’s icon; tap it to pick another view. Each view takes the whole screen.
If the window grows or shrinks between the two layouts, an open view follows: from the phone view to the desktop panel, and back.
## Which Directory Is Shown
[Section titled “Which Directory Is Shown”](#which-directory-is-shown)
The root is the **working directory of the pane on screen**. When that directory is inside a git repository, the root is the top of the repository instead, so Files and Changes cover the whole repository. The root is shown at the top of both views, with `~` for your home directory.
The directory is the one of the program running in the pane, so it follows a `cd` in the shell. A command that only passes through another directory for a moment does not move the root. When the root does move while a view is open, the view reloads from the new root and says `The pane's directory changed`.
Supported backends:
* **tmux** on Linux and macOS, natively or in the container
* **Herdr** on any OS. Herdr must report the pane’s directory (`cwd`); an older Herdr that does not shows `Not supported by this backend`.
* **psmux** on Windows. The views follow `cd` in PowerShell 7 (`pwsh`) and in `cmd.exe`. Windows PowerShell 5.1 (`powershell.exe`) does not tell Windows when you change directory, so the views stay on the directory the shell started in. Use PowerShell 7, or add this line to the `prompt` function in your 5.1 profile: `[Environment]::CurrentDirectory = $PWD.ProviderPath`.
### Places
[Section titled “Places”](#places)
Files can also show a folder outside the pane, such as a shell config folder or your notes, once it is named on the host:
```bash
termote start --allow-path ~/.config/fish --allow-path ~/notes
termote start --remove-path ~/notes
termote container up --allow-path ~/notes # the container keeps its own list
```
Pick it from **Places** (the folder icon at the top of Files); **Pane folder** goes back. A Place works like the pane’s folder: browse, search, edit, create, rename, delete with Undo, download and paste images. Changes always stays on the pane’s repository. Places are listed only to a device with full access, never to a view-only one, and the app can never add or remove one.
Termote refuses your home folder itself and any folder above it, `~/.ssh`, `~/.gnupg`, its own config, state, data and cache folders (and any folder holding them, such as `~/.config`, `~/.cache` or `~/.local`; `~/.config/fish` is fine), `/proc`, `/sys`, `/dev`, the folders holding the host’s control sockets (`/tmp`, `/run`, `/var/run`), and a `.git` folder. A saved folder that is gone or now points somewhere refused is skipped, and `start` prints the `--remove-path` that drops it.
## Files
[Section titled “Files”](#files)
* Folders come first, then files, ignoring case. Tap a folder to open or close it.
* A folder shows at most 5000 entries. A longer folder says that only the first 5000 are shown.
* Devices, sockets and links that point outside the root are greyed out and do not open. One exception: in a git worktree, a link to a file or folder in another worktree of the same repository (such as `docs/records` linked to the main checkout) opens like one of the root’s, read-only. Find and Search look inside such a folder when git does not track the link (and the link is not inside a folder git does not track), and sort its files as that worktree’s git does: a file it ignores is found only with ignored files on, and never searched. Windows junctions are not followed, only symbolic links.
* A file opens in place of the tree, in a tab (see [Open Files as Tabs](#open-files-as-tabs)), with its path, its size, a button to copy the path and one to wrap long lines (on by default on a phone). Long lines scroll inside the code area, never the whole page.
* Highlighting covers common languages and file names (TypeScript, Go, Python, shell, Dockerfile, Makefile, YAML, JSON, Markdown and others). It loads the first time a file needs it, so a device that never opens Files never downloads it.
* A file of more than 2000 lines keeps only the lines near the view in the page, so it opens and scrolls as quickly as a short one, whatever its length. The browser’s find (Ctrl+F) and a selection then reach only those lines: use Search in Files, or download the file. A file of more than 400,000 lines shows its first 400,000 and says how many more there are.
| Shown | Why |
| ------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------ |
| Text with colours | A text file of up to 256 KiB, up to 5000 lines, no line over 2000 characters |
| Plain text, no colours | A larger text file (up to 4 MiB), a language not covered, or highlighting that took over 3 seconds |
| `Not previewable (binary, special file or larger than 4 MiB)` | A binary file that is not an image or a document, a file over 4 MiB, or something that is not a regular file |
| `This file can't be shown` | A path termote never serves (see below) |
## Right-Click Menus
[Section titled “Right-Click Menus”](#right-click-menus)
A right click on a file or folder of the tree opens its menu where the pointer is; the **…** button on the row, a long press on a phone, Shift+F10 or the menu key open the same menu by the row, its first item focused.
* **Copy relative path**, **Copy absolute path** and **Copy name**. The relative path is from the root the tree shows; the absolute one uses the host’s separator (`\` on Windows), and a link’s is the link’s own path, not where it leads.
* **Insert @path in terminal** types the path after `@` into the pane, without Enter, and shows the terminal. A file under the pane’s directory gets a path from there (`@src/app.ts`), any other (above it, beside it, or in a Place) its absolute path; one with a space is quoted (`@"my notes.md"`), as Claude Code writes it. The pane’s directory is read at that moment, so a `cd` in the pane counts. A name with a line break or a `"` is never typed (it could run a command in a shell): a toast says so. When the terminal is not connected, a toast offers to copy the path instead.
* On a folder: **New file**, **New folder** and **Upload files here**, the box starting in that folder. A link to a folder, or a folder reached through one, offers none of them.
* **Download**, **Rename** and **Delete**, as described below.
A right click below the rows opens the tree’s own menu: **New file**, **New folder**, **Upload files** (into the root), **Refresh** and **Collapse all**. An empty directory shows a message in place of the tree; a right click on it opens the same menu, without **Collapse all**, so a first folder can be made there.
On a desktop, a right click on a file’s tab opens the same menu with **Keep open** (for a preview tab) and **Close** first. A tab kept from a directory the pane has left only copies, types its absolute path and closes.
A view-only device sees the copies, **Refresh** and **Collapse all** only (and, on a tab, **Keep open** and **Close**).
## Open Files as Tabs
[Section titled “Open Files as Tabs”](#open-files-as-tabs)
Opening a file keeps the ones already open: each has a tab, and going back to a tab brings back where you were in that file and its unsaved changes.
* **A single click opens a preview tab**, its name in italics: the next file opened the same way replaces it. A file gets a tab of its own once you double-click it (in the tree or on its tab), start editing it, create it, or tap **Keep open** (the pin in its bar).
* A file that is already open shows its tab; it never opens twice.
* The first tab, **Files**, shows the tree; the files stay open behind it. Back, when no link is left to retrace, shows the tree too.
* **Closing a tab**: its × button, a middle click, or Delete on the focused tab. A tab with unsaved changes (a dot after its name) asks `Discard changes?` first.
* **Reordering tabs** (desktop): drag a tab along the bar with the mouse; the others slide aside to make room, and Escape puts it back. Alt+Left or Alt+Right moves the focused tab one place. The **Files** tab always stays first. Diff tabs in Changes move the same way.
* **At most 10 tabs.** Opening one more closes the tab used longest ago, never the one shown and never one with unsaved changes. When every other tab has unsaved changes, more than 10 stay open.
* **On a phone** there is no tab bar. The button with the number of open files (`Open files (N)`), in the file’s bar and above the tree, lists them in a sheet: tap one to show it, or keep it open, or close it.
* **Refresh** also reads the file shown again, so a change made outside Termote appears without reopening it. Other tabs read their file again when you show them. A file you are editing is not read again: your changes stay, and saving still refuses a file that changed since you opened it.
* A file deleted in Termote closes its tab, and a file renamed in Termote keeps its tab under the new name. A file deleted or renamed by something else keeps its tab, which shows `File not found` with **Close tab**; once shown, the tab’s name is struck through in the tab bar and the open files sheet until the file is found again.
* When the pane moves to another directory, the tabs opened in the old one close. Tabs with unsaved changes ask first: **Discard and close** drops the changes, **Keep** leaves them open so you can copy their text (they can no longer be saved).
* A file that may contain secrets asks again in every new tab. A tab where you chose to show it keeps it shown until the tab closes.
* Tabs live only in the page’s memory: reloading the app starts with none.
## Finding a File
[Section titled “Finding a File”](#finding-a-file)
The **Find a file…** box at the top of Files (above the keyboard on a phone) searches the names of the files under the root. Results replace the tree after a moment’s pause in typing; each row shows the path.
* Tap a result, or press Enter on it, to open the file; the tree opens down to it, and Back returns to the results. The arrow keys move between the box and the results, and Escape clears the box.
* A file that git ignores is not searched unless **Include ignored** (the switch next to the box, shown only in a git repository) is on. Those results are dimmed and tagged `ignored`. The choice is remembered on this device.
* A name that usually holds secrets shows a lock, as in the tree. Only names are searched here; the contents of files are searched in the **Search** tab (see [Searching File Contents](#searching-file-contents)).
* Only the first 200 matches are shown (`More than 200 matches — type more to narrow`). In a very large repository or folder the search can stop early, and the list says `Search stopped early in a large tree`.
* The list of files is kept for 30 seconds, and saving, creating or deleting a file drops it. **Refresh** reads it again.
* Outside a git repository, the folder is walked (at most 32 levels deep, for 5 seconds), and linked folders are not entered.
The folders never searched for ignored files are set in Settings → Files → **Excluded folders** (see [Settings](/usage/settings/#excluded-folders)). A file git tracks is always found, even under one of them.
Paths Termote never serves (see [Never Shown](/usage/security/#never-shown)) never appear in the results.
## Searching File Contents
[Section titled “Searching File Contents”](#searching-file-contents)
The **Files | Search** switch at the top of Files shows the **Search** tab, which searches the text of the files under the root. On a desktop, Ctrl+Shift+F (Cmd+Shift+F on a Mac) opens it and puts the cursor in the box, from anywhere in Files or elsewhere in the app (the Files panel opens first); from the terminal the keys stay the terminal’s. The key can be changed in [Settings > Shortcuts](/usage/keyboard-shortcuts/#changing-keys).
* The search runs as you type, after a short pause, once the box holds at least 2 characters. The text is searched as typed: spaces at either end count.
* **Match case**, **Whole word** and **Regular expression** (the buttons next to the box) change how the text matches; they are remembered on this device. Without Match case, case is ignored. A regular expression uses Go’s syntax (RE2); whole words are made of the letters `A`–`Z`, `a`–`z`, digits and `_`. A regular expression that can match empty text, such as `a*`, is refused (`This pattern matches empty text`): write `a+` instead.
* The **…** button shows two more boxes: **Files to include** and **Files to exclude**, each a comma-separated list of patterns such as `*.ts, src/`. A pattern without `/` matches the file name in any folder; `**` stands for any number of folders; a pattern ending in `/` matches everything in that folder.
* Results are grouped by file, with the number of matching lines; each line shows its number and the matches highlighted. Groups past the first 50 files start closed. Tap a line, or press Enter on it, to open the file at that line, highlighted in the middle of the view. Markdown, CSV/TSV and SVG files open as their source in that tab, whatever the preview settings say. Back returns to the same results. The arrow keys move between the box and the results, and Escape goes back to the box.
* At most 2000 lines are shown (`Showing the first 2000 lines`), and 100 lines per file (the rest is counted under the file). A search stops after 5 seconds or 256 MiB read (`Stopped early: some files were not searched`); a line longer than 16 KiB is searched on its start only. Two searches can run at once on the server; a third says `Search busy, try again`.
* If the pane’s directory changed since the search, the results are dimmed and cannot be opened until the next search.
The files searched are the ones **Find a file** lists without **Include ignored**. These are never searched:
* files git ignores, in a git repository;
* files that may contain secrets (see [Files That May Contain Secrets](/usage/security/#files-that-may-contain-secrets)), counted under the results (`2 sensitive files not searched`);
* dot-files and dot-folders when the folder shown is your home folder or holds it (shell histories and tool settings hold secrets under ordinary names), also counted;
* binary files, files that are not UTF-8 text (a Shift-JIS, EUC-JP or UTF-16 file opens in Files, but is not searched), and files larger than 1 MiB;
* paths Termote never serves (see [Never Shown](/usage/security/#never-shown)).
A view-only device searches the same files it can open.
## Downloading a File
[Section titled “Downloading a File”](#downloading-a-file)
A copy of a file can be saved on the device you are using: **Download** in the **⋯** menu of its row in the tree (on a phone, a finger resting on the row opens it), or the **Download** button in the header of an open file (in the header’s menu on a phone). The browser saves it as it saves any download, under the file’s name; nothing is opened or shown, whatever the file holds.
* Any regular file can be downloaded, also one Termote cannot preview (binary, larger than 4 MiB), up to **100 MiB**.
* A file whose name usually holds secrets is downloaded after a second ask; an open one you chose to show is downloaded at once.
* Only files are downloaded, not folders or links, and never through a folder that is a link.
* In the iOS app added to the Home Screen, the file opens in the share sheet: pick **Save to Files**. Closing the sheet saves nothing.
* Every download is written to the server log as an `audit: download` line (the path inside the pane’s directory, the size and who asked).
| Shown | Why |
| ------------------------------------------------------ | ---------------------------------------------------------------------- |
| `<name> is larger than 100 MiB, too large to download` | The file is over the limit |
| `Too many files at once; try again` | Other downloads or images are being read; try again in a moment |
| `<name> is gone` | Something removed or moved the file after the tree was read |
| `<name> is not a regular file; not downloaded` | A FIFO, a socket or a device |
| `A link on that path; not downloaded` | The file or a folder on its path is a symbolic link |
| `<name> can't be downloaded from here` | Termote’s own folders, the install’s data, `.git` and the upload store |
| `<name> changed; try again` | Something replaced the file while it was checked |
A view-only session has no Download.
## Uploading Files
[Section titled “Uploading Files”](#uploading-files)
A file on the device you are using, of any type (a PDF, a CSV, a zip, a screenshot, a log), can be put in a folder of Files in three ways:
* **Upload files** (an arrow up, next to New file) opens the device’s file picker; on a phone it offers the photo library, the camera and the Files app too. Several files can be picked at once. They go into the folder you last tapped in the tree, or the folder of the file you last tapped, or the top folder.
* **Dropping** files from the computer’s file manager on Files: on a folder row they go into that folder, on a file row into its folder, below the rows into the top folder. A folder that is a symbolic link, or one inside such a link, passes them to the nearest real folder above it. While a file is open, a drop puts the files in that file’s folder, and the result names the folder. A dropped file is never inserted into the text being edited.
* **Pasting** files copied in the file manager (Ctrl+V, ⌘V on a Mac) while the tree is shown puts them in the same folder as Upload files. A paste into the search box, or with a file open, does nothing. What a copy in the file manager puts on the clipboard differs between browsers and systems (on Linux often only the file’s path), so when nothing happens, use the button or a drop.
The files go up one at a time. A row under the Files header shows the file being sent, how far it is and how many are left, with **Cancel**; Cancel turns off once a file is fully sent, since the server then saves it anyway. Uploads carry on when you switch to the terminal or another view, each into the folder it was started for; the row shows again when you come back to Files. When they are done one notice says how it went, such as `Uploaded 3 files.` or `1 of 3 uploaded. big.zip: File is larger than 100 MB (1 more not uploaded)`.
* At most **100 MiB** a file, the most a download takes.
* An existing file is never replaced: a taken name gets a number, `report.pdf` becoming `report (2).pdf`, up to `(99)`. The notice says `Saved as report (2).pdf`.
* A folder that usually holds secrets (`.ssh`, `.aws`, …), or a name that does (`.env`), is asked about first, once for the files sent together, naming the file and where it goes. Enter keeps the files away; answering no sends none of those files, without a notice.
* Only files go up, never folders: a dropped folder is skipped and said so.
* When another upload is running (from another device, or another tab), the row says `Waiting for another upload…` and the file goes once that one ends, or after two and a half minutes says so.
* When the connection drops after the file was fully sent, Files reads the folder to see whether the file arrived; if it cannot tell, it says `Not sure the file was saved. Check the folder`.
* A file is written under a hidden temporary name (`.termote-upload-…`) and only gets its name once complete, so a half-sent file never shows under its name (while it is sent, the temporary file may show in the tree, marked as sensitive). One left by a server stopped mid-upload is removed by the next upload to that folder after an hour.
* Every upload is written to the server log as an `audit: upload` line (the path, the size and who sent it).
| Shown | Why |
| ------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `File is larger than 100 MB` | The file is over the limit |
| `The folder is a link: not uploaded` | A folder on the path is a symbolic link the tree had not shown as one: nothing is written through one |
| `Files can't be uploaded to this folder` | A path Termote never serves (see [Never Shown](/usage/security/#never-shown)), `.git`, or its own folders |
| `This file's name can't be used on the host` | A name with `\`, a Windows device name (`NUL`, `CON.txt`, `COM1`), invisible or direction-changing characters, a control character, more than 255 bytes, one ending in a dot or a space, or on a Windows host one of `<>:"\|?*` |
| `Too many files with this name. Rename one first` | `name (2)` up to `name (99)` are all taken |
| `The folder is no longer on the host` | Something removed or moved the folder after the tree was read |
| `The server can't write in this folder` | The folder cannot be written |
| `The file system there is read-only` | The folder is on a read-only disk or mount |
| `The host's disk or quota is full` | The host’s disk, or your disk quota, has no room left |
| `The upload stalled. Try again on a better connection` | Nothing arrived for two minutes, or the file took more than 30 minutes |
| `Another upload is running. Try again shortly` | Another device’s upload still held the server after the wait |
Caution
An uploaded file is as trusted as one made with New file: a tool may read or run it by itself (on Windows a file runs by its extension, so an uploaded `.exe` or `.ps1` can be started like any other). Anyone who can sign in can already run commands in the terminal, so this allows nothing new, but check what you upload into such places.
A view-only session has no Upload files button and pastes nothing; a drop only says `A view-only device cannot upload`, and the browser never opens the dropped file.
# Mobile Gestures
> Swipe gestures for the terminal on a phone in Termote: Ctrl+C, Tab, scrolling, font size, paste and copy, with a full gesture reference.
Termote supports touch gestures for common terminal operations.
## First-Time Tutorial
[Section titled “First-Time Tutorial”](#first-time-tutorial)
On your first visit, the **welcome screen** sums up the gestures, and its **1-minute tour** ends on the terminal with the main ones. Open either again from **Settings → Data & help** (**Show welcome screen**, **Take the tour**); the full list is in **Help & gestures** in the **More** menu.
## Gesture Reference
[Section titled “Gesture Reference”](#gesture-reference)
| Gesture | Action | Terminal Command |
| ----------- | ---------------- | ---------------- |
| Swipe left | Cancel/interrupt | Ctrl+C |
| Swipe right | Tab completion | Tab |
| Drag up | Scroll down | - |
| Drag down | Scroll up | - |
| Long press | Paste clipboard | Paste |
| Pinch in | Decrease font | - |
| Pinch out | Increase font | - |
Dragging up or down moves the history with your finger, and a flick keeps it gliding until it slows down or you touch the screen again. With the tmux backend the history is scrolled in tmux copy mode, one page per swipe.
## Gesture Zone
[Section titled “Gesture Zone”](#gesture-zone)
Gestures work in the terminal area only. The header, the keyboard toolbar, and sheets are excluded.
Note
Gestures are disabled on desktop where mouse interactions are preferred. On mobile, use gestures for quick actions or tap the **Extra keys** button (⋯) in the keyboard toolbar: its Actions row gives immediate access to Clear, Cancel, Clear line, and Exit commands.
## Notifications
[Section titled “Notifications”](#notifications)
A notification near the top of the screen closes on its own after a few seconds. To close it sooner, swipe it up or to either side. While your finger holds it, it stays; letting go starts its time again, and a short swipe puts it back. This works with a mouse too.
## Font Size
[Section titled “Font Size”](#font-size)
* Range: 6px - 24px
* Default: 14px
* Adjust with pinch or toolbar buttons
* Applies to the terminal only: the controls and pinch show or work only in the terminal view
With the Herdr backend, and [Fit herdr pane to this device](/usage/settings/) off, the pane keeps the desktop’s size and the font works as a zoom on a screen too small for it: 14 fits the pane to the screen, and each step up or down makes the text 1px larger or smaller. A pane larger than the screen moves with your finger: sideways, and up or down until its edge, after which the drag scrolls the history.
## Clipboard Paste
[Section titled “Clipboard Paste”](#clipboard-paste)
Long press paste may not work on all browsers due to clipboard API restrictions:
* **iOS Safari**: Long press not supported, use the **Paste button** instead
* **Chrome mobile**: Works with HTTPS
* **Other browsers**: May require clipboard permission
If paste fails, a toast notification will guide you to use the Paste button or text input.
## Copy Text
[Section titled “Copy Text”](#copy-text)
A touch on the terminal is a gesture, so text cannot be selected on the terminal itself. Tap the **Select text** key in the toolbar: a sheet shows the pane’s text with its history, where a long press and the selection handles select as anywhere else on the phone. Tap **Copy** for the selection or **Copy all**. See [Copy text](/usage/keyboard/#copy-text).
## Tips
[Section titled “Tips”](#tips)
1. **Quick cancel**: Swipe left to send Ctrl+C when a command hangs
2. **Tab complete**: Swipe right to autocomplete file/command names
3. **Scroll back**: Swipe down to read earlier output, swipe up to return (the same as the toolbar’s scroll keys)
4. **Paste issues**: If long press doesn’t work, use the Paste button in the toolbar
5. **Copy**: Use the Select text key; a long press on the terminal pastes, it never selects
# Herdr Plugin
> Open the Herdr pane you are on in Termote, show its link as a QR code for a phone, and start or stop the server, without leaving Herdr
When Termote runs the [Herdr backend](/usage/sessions/#herdr-backend), the Termote plugin for Herdr gives you its link from inside Herdr: it opens Termote on the workspace, tab and pane you are on (a [deep link](/usage/sessions/#deep-links)), shows that link as a QR code to scan with a phone, copies it, and starts, stops or restarts the server.
## Requirements
[Section titled “Requirements”](#requirements)
* Herdr 0.7.4 or later.
* Termote 1.5.0 or later (it has `termote url` and `termote panel`; update with `termote update`). The plugin runs the `termote` command for everything it does, so it needs no other runtime.
* Linux and macOS: Herdr starts plugin commands with its server’s `PATH`, which often lacks `~/.local/bin`. The plugin’s launcher also looks there and in the install (`~/.local/share/termote/current/bin`, or under `$XDG_DATA_HOME`), so `termote link` is not required, and skips a copy too old for the plugin. When it finds none that works, the panel says why and stays open until you press Enter.
* Windows: the installer puts `termote` on the user `PATH`; restart Herdr after installing so it sees it.
* The server runs the Herdr backend (`termote start --mux herdr`). With tmux, the IDs in a Herdr link mean nothing to Termote, so the link opens the home screen and the plugin says so.
## Install
[Section titled “Install”](#install)
```bash
herdr plugin install lamngockhuong/termote/herdr-plugin
```
Herdr shows the manifest and the commands it will run before installing. To remove it: `herdr plugin uninstall lamngockhuong.termote`.
## The Panel
[Section titled “The Panel”](#the-panel)
The panel is a popup with the server’s status, every address it answers on (Tailscale, LAN, this machine), and a QR code of the link to the pane that was focused when you opened it:
```bash
herdr plugin action invoke lamngockhuong.termote.panel
```
| Key | Does |
| ---------- | ----------------------------------------------- |
| `o` | Open the link in this machine’s browser |
| `c` | Copy the link |
| `s` | Start the server (shown when it is not running) |
| `x` | Stop the server |
| `r` | Restart the server |
| `q`, `Esc` | Close the panel |
The link uses the first address another device is likely to reach: the Tailscale name, else the first LAN address, else `127.0.0.1`. The password is never part of it; the device that opens it still signs in.
## Actions
[Section titled “Actions”](#actions)
The same tasks are plugin actions, for a key binding or `herdr plugin action invoke`. Herdr allows each id once, so on Windows the ids end in `-windows` (`lamngockhuong.termote.panel-windows`, `lamngockhuong.termote.open-windows`, …):
| Action | Runs |
| ------------------------------- | ---------------------------- |
| `lamngockhuong.termote.panel` | Opens the panel |
| `lamngockhuong.termote.open` | `termote url --herdr --open` |
| `lamngockhuong.termote.copy` | `termote url --herdr --copy` |
| `lamngockhuong.termote.start` | `termote start` |
| `lamngockhuong.termote.stop` | `termote stop` |
| `lamngockhuong.termote.restart` | `termote restart` |
Herdr only writes an action’s output to the plugin log (`herdr plugin log list --plugin lamngockhuong.termote`), so use the panel when you want to read the result. To open the panel with a key, add this to Herdr’s `config.toml`:
```toml
[[keys.command]]
key = "prefix+o"
type = "plugin_action"
command = "lamngockhuong.termote.panel"
description = "Termote link and QR code"
```
On Windows, bind `lamngockhuong.termote.panel-windows` instead.
Note
`copy` uses the system’s clipboard command (`wl-copy`, `xclip`, `xsel`, `pbcopy`, `clip`). An action has no terminal, so when none of them works there (for example `xclip` without a display), use `c` in the panel: it falls back to the terminal’s clipboard (OSC 52), which also works over SSH.
## From a Script
[Section titled “From a Script”](#from-a-script)
The plugin is a thin layer over two commands you can use yourself:
* `termote status --json` prints the server’s state as one JSON object: `running`, `status`, `version`, `backend`, `pid`, `port`, `lan`, `auth`, `tailscale`, `urls` (`local`, `lan`, `tailscale`) and `supervisor`. It exits with 1 when the server is not running.
* `termote url` prints the link: `--group`, `--tab` and `--pane` select a session (the IDs of `/api/mux/snapshot`), `--view` a view, and `--herdr` takes the session from the Herdr plugin context. `--open`, `--copy` and `--qr` open it, copy it, or print it as a QR code. Only the link goes to stdout.
```bash
termote url --group main --tab 1 # tmux: session "main", window 1
termote url --group w1 --tab w1:t2 --qr # Herdr: workspace w1, tab w1:t2, with a QR code
```
# Keyboard Toolbar
> The Termote keyboard toolbar for phones: Tab, Esc, Ctrl and arrow keys, modifiers, command history, image attach and non-Latin input.
The keyboard toolbar provides touch-friendly buttons for common terminal keys. Its bottom row holds the keys used most; the **Extra keys** button (⋯), pinned at its right end, shows the expanded rows with every other key.
On a desktop keyboard, the app’s own keys and the command palette are in [Keyboard Shortcuts & Command Palette](/usage/keyboard-shortcuts/).

## Bottom Row
[Section titled “Bottom Row”](#bottom-row)
| Button | Key / action |
| ------ | ------------------------------------------- |
| ⌨️ | Show/hide the virtual keyboard |
| 🌐 | Text input mode (IME) for non-Latin scripts |
| Esc | Escape (clears Ctrl/Shift when one is on) |
| Ctrl | Ctrl modifier (sticky) |
| ↑ ↓ | Up/Down arrows |
| Enter | Submit command |
| Tab | Autocomplete |
| ⋯ | Extra keys: show or hide the expanded rows |
When the row does not fit the screen it scrolls sideways, and the edge that still hides keys fades out. The ⋯ button stays at the right end, outside the scrolling part, so it is always on screen.
## Expanded Rows
[Section titled “Expanded Rows”](#expanded-rows)
Tap ⋯ (or turn on **Toolbar default expanded** in [Settings](/usage/settings/)) to show labelled rows above the bottom row:
| Row | Keys |
| ---------------- | --------------------------------------------------------------------------------- |
| Actions (mobile) | Clear, Cancel, Clear line, Exit: see [Actions row](#actions-row-mobile-only) |
| Text · Scroll | Command history, Paste, Attach image, Select text, tmux copy mode, scroll up/down |
| Navigate | Shift, Shift+Tab, ←, →, Home, End, PgUp, PgDn, Del, Bksp, Ins |
The rows close by themselves when the on-screen keyboard opens and when you open the command history, so they never cover the whole terminal together; tap ⋯ to show them again.
While Ctrl is on, its combos float above the toolbar instead of adding a row, so the terminal keeps its size and a running full-screen program does not redraw. Ctrl+Shift combos float the same way.
## Modifiers
[Section titled “Modifiers”](#modifiers)
| Button | Key | Where |
| ------ | ------- | ------------ |
| Ctrl | Control | Bottom row |
| Shift | Shift | Navigate row |
### Modifier Combinations
[Section titled “Modifier Combinations”](#modifier-combinations)
* **Ctrl only**: Shows Ctrl combos (^C, ^D, etc.)
* **Shift only**: Modifies next key; the **⇧Tab** key sends Shift+Tab in one tap. Closing the expanded rows turns Shift off, since its key is no longer on screen
* **Ctrl+Shift**: Shows Ctrl+Shift combos (^⇧C, ^⇧V, etc.)
## Keys
[Section titled “Keys”](#keys)
### Ctrl Combos
[Section titled “Ctrl Combos”](#ctrl-combos)
| Button | Combo | Use Case |
| ------ | ------ | ---------------- |
| C | Ctrl+C | Cancel/interrupt |
| D | Ctrl+D | EOF/logout |
| Z | Ctrl+Z | Suspend |
| L | Ctrl+L | Clear screen |
| A | Ctrl+A | Line start |
| E | Ctrl+E | Line end |
| B | Ctrl+B | tmux prefix |
| X | Ctrl+X | Cut/prefix |
| K | Ctrl+K | Delete to end |
| U | Ctrl+U | Delete to start |
| W | Ctrl+W | Delete word |
| R | Ctrl+R | Reverse search |
| P | Ctrl+P | Previous command |
| N | Ctrl+N | Next command |
### Ctrl+Shift Combos
[Section titled “Ctrl+Shift Combos”](#ctrlshift-combos)
| Button | Combo | Use Case |
| ------ | ------------ | ------------------------------------------ |
| C | Ctrl+Shift+C | Copy the selection, else send Ctrl+Shift+C |
| V | Ctrl+Shift+V | Paste |
| Z | Ctrl+Shift+Z | Redo |
| X | Ctrl+Shift+X | Cut |
### Text · Scroll Keys
[Section titled “Text · Scroll Keys”](#text--scroll-keys)
| Button | Action |
| ------------------------ | --------------------------------------------------------------------------------- |
| Copy mode icon (history) | Toggle tmux copy mode (hidden with the Herdr backend) |
| Select text icon | Open the pane’s text to select and copy: see [Copy text](#copy-text) |
| Paste icon (clipboard) | Paste from the source chosen in [Settings](/usage/settings/) (default: clipboard) |
| Image icon | Attach an image: see [Attach an image](#attach-an-image) |
| ⇈ | Scroll up: a page of tmux history, or the terminal scrollback with Herdr |
| ⇊ | Scroll down: a page of tmux history, or the terminal scrollback with Herdr |
### tmux Copy/Paste Workflow
[Section titled “tmux Copy/Paste Workflow”](#tmux-copypaste-workflow)
1. Tap the **copy mode icon** to enter copy mode
2. Use arrow keys to navigate
3. Press **Space** to start selection
4. Navigate to select text
5. Press **Enter** to copy to tmux buffer
6. Tap the **Paste icon** to paste (with the paste source set to tmux buffer in [Settings](/usage/settings/))
Herdr backend
Herdr has no tmux copy mode or paste buffer, so the copy mode key is hidden, the Select text key takes its place, and the Paste icon always uses the system clipboard.
### Copy text
[Section titled “Copy text”](#copy-text)
The tmux workflow above copies into the tmux buffer only. To put terminal text on the device’s own clipboard:
* **Select text key** (phone and desktop, tmux and Herdr): opens a sheet with the pane’s text, its history included (the last 1000 lines; **Load more** reads up to 5000). Select with a long press and the selection handles, or drag with a mouse, then tap **Copy**; **Copy all** copies everything. Hidden characters (bidi marks, zero-width spaces) show as `⟨U+XXXX⟩` and are copied as they are. On a view-only device, or where the server cannot read the history (psmux), the sheet shows what the terminal on the page holds, and says so.
* **Ctrl+Shift+C** (desktop, not on a Mac): with text selected by the mouse, copies it and sends nothing to the terminal; without a selection the keys reach the program as before. The ^⇧C toolbar combo does the same. On a Mac, Cmd+C copies the selection.
* **Copy on select** in [Settings](/usage/settings/): copies as soon as you release the mouse button. Where the program takes the mouse (tmux mouse on, vim `mouse=a`), hold Shift while dragging (Option on a Mac).
A page opened over plain HTTP (a LAN address) has no Clipboard API; copying falls back to the browser’s copy command and shows an error when that is refused too. The Herdr terminal redraws its whole screen when its stream starts again, which clears a mouse selection.
Native Mode Setup
For native mode, add this to your `~/.tmux.conf`:
```bash
set-option -g mode-keys vi
```
Container mode has this pre-configured.
### Navigate Keys
[Section titled “Navigate Keys”](#navigate-keys)
| Button | Key | Use Case |
| ------ | --------- | ----------------------------------------- |
| ⇧Tab | Shift+Tab | Back in menus; switch mode in Claude Code |
| ← → | Arrows | Move the cursor |
| Home | Home | Jump to line start |
| End | End | Jump to line end |
| Del | Delete | Delete forward |
| Bksp | Backspace | Delete backward |
| PgUp | Page Up | Scroll up |
| PgDn | Page Down | Scroll down |
| Ins | Insert | Toggle insert mode |
## Text Input Mode (Non-Latin)
[Section titled “Text Input Mode (Non-Latin)”](#text-input-mode-non-latin)
Tap the **Languages icon** to switch to text input mode. This is designed for typing non-Latin scripts (Vietnamese, Chinese, Japanese, Korean, Thai, etc.) that require IME composition.
| Action | Result |
| --------- | ----------------------------------------------------------------------------- |
| Type text | Compose using your device’s IME |
| Tap Send | Send text to terminal (behavior configurable in [Settings](/usage/settings/)) |
| Tap ✕ | Close text input, return to key toolbar |
## Touch Targets
[Section titled “Touch Targets”](#touch-targets)
* Minimum size: 44x44px
* Spacing: 8px between buttons
* Haptic feedback on tap (if device supports)
## Command History
[Section titled “Command History”](#command-history)
Tap the **clock icon** (🕐) in the Text · Scroll row of the expanded toolbar to open the command history dropdown. View, search, and reuse previously sent commands.
### Features
[Section titled “Features”](#features)
| Feature | Behavior |
| ------------------- | ------------------------------------------------------------ |
| Search bar | Filter commands by typing |
| Keyboard nav | Use Up/Down arrows to navigate, Enter to select, Esc to exit |
| Delete button | Remove individual commands from history |
| Clear all button | Wipe entire history at once |
| Delete via settings | Clear history via Settings → “Clear Command History” |
### How It Works
[Section titled “How It Works”](#how-it-works)
* **Automatic capture**: Commands are saved when you press Send in text input mode (IME)
* **Persistence**: History saved to browser local storage
* **Per-browser**: History is device/browser-specific, not synced across devices
* **Search**: Filter by partial command name for quick access
Note
Commands sent via keyboard toolbar buttons (Ctrl+C, etc.) are not added to history — only IME text submissions are captured.
## Actions Row (Mobile Only)
[Section titled “Actions Row (Mobile Only)”](#actions-row-mobile-only)
On a phone the expanded toolbar starts with an **Actions** row of common terminal actions. Tap ⋯ to show it.
| Action | Function | Sends |
| ---------- | ------------------- | --------------- |
| Clear | Clear terminal | `clear` command |
| Cancel | Interrupt process | Ctrl+C |
| Clear line | Delete current line | Ctrl+U |
| Exit | Logout/EOF | Ctrl+D |
Note
The Actions row appears on mobile devices only. On desktop, use the keyboard toolbar keys instead.
## Attach an Image
[Section titled “Attach an Image”](#attach-an-image)
Claude Code reads an image you paste, but only from the clipboard of the machine it runs on, which is empty when the image is on your phone. Termote uploads the image to that machine instead and types its path into the terminal, followed by a space; Claude Code (or Codex) turns the path into an attachment.
* **Image key** in the Text · Scroll row of the expanded toolbar: pick a photo, or take one with the camera.
* **Paste**: paste an image into the terminal (Ctrl+V on desktop), or tap the Paste key, long press the terminal or press Ctrl+Shift+V with an image on the clipboard. When the clipboard holds text too, the text is pasted, not the image.
PNG, JPEG, GIF and WebP up to 10 MB are accepted. iOS normally sends a photo as JPEG; a HEIC file is refused, share it as JPEG instead. If you switch to another pane while the image uploads, nothing is typed: a message offers to insert the path into the pane you are on.
The images are saved on the host in the user’s cache directory (`~/.cache/termote/uploads` on Linux, `~/Library/Caches/termote/uploads` on macOS, `%LOCALAPPDATA%\termote\uploads` on Windows), readable only by that user. They are deleted after 7 days, or sooner, oldest first, when they take more than 200 MB. The image key is hidden when the server cannot store uploads, and for a view-only session.
## Editing a File
[Section titled “Editing a File”](#editing-a-file)
These keys are for the editor of Files and Changes on a computer with a mouse, not for the terminal; the full list is in [Editing a File](/usage/file-editing/#editing-a-file).
| Keys | What they do |
| ----------------------- | ---------------- |
| Ctrl+S (Cmd+S on a Mac) | Save |
| Ctrl+F (Cmd+F on a Mac) | Find in the file |
| Ctrl+H | Find and replace |
| Ctrl+G | Go to a line |
## Tips
[Section titled “Tips”](#tips)
1. **Modifiers are sticky**: Tap Ctrl or Shift once, then tap another key
2. **Combine modifiers**: Tap both Ctrl and Shift for Ctrl+Shift combos
3. **Shift+Tab**: The ⇧Tab key navigates backwards in completion/menus
4. **Copy mode** (tmux): Enable to select text, disable to resume typing; to copy to the device, use [Select text](#copy-text)
5. **Extra keys**: Tap ⋯ for the expanded rows with Shift, Home, End, etc.
6. **Command history**: Use 🕐 clock icon to quickly reuse previous commands
7. **Actions**: On a phone, the Actions row has Clear, Cancel, Clear line and Exit
# Keyboard Shortcuts & Command Palette
> Keyboard shortcuts and the command palette of Termote on a desktop: go to a file, run a command, move between session tabs and change any key.
On a desktop (a mouse or a trackpad, and a window wider than a phone’s), one palette reaches the pane’s files and every command of the app from the keyboard. A phone keeps the [keyboard toolbar](/usage/keyboard/) and has no palette.
## Opening the Palette
[Section titled “Opening the Palette”](#opening-the-palette)
| Keys | Opens |
| ---------------------------------------------------- | ----------------------------- |
| Ctrl+P (Cmd+P on a Mac) | Go to a file of the pane |
| Ctrl+Shift+P (Cmd+Shift+P on a Mac) | Every command |
| Ctrl+Shift+F (Cmd+Shift+F on a Mac) | Search in files (Files panel) |
| Command palette in the ⋯ menu, or ⌘ on the right bar | Every command |
What the palette lists depends on the first character typed in its box; delete it, or type another, to switch on the spot:
| Starts with | Lists |
| ----------- | ---------------------------------------------------- |
| (nothing) | Files of the pane on screen |
| `>` | Commands |
| `:` | A line of the file open in Files, `:120` or `:120:5` |
| `@` | Tabs of every group, and the panes of this tab |
↑ and ↓ move through the list (from the last row back to the first), Page Up and Page Down move ten rows, Enter runs the row and Escape closes the palette. Pressing the key that opened a mode again moves down the list. In the `@` list, Ctrl+Enter (Cmd+Enter) opens the tab or pane in a [new window](/usage/sessions/#open-in-a-new-window) instead of switching to it.
## Going to a File
[Section titled “Going to a File”](#going-to-a-file)
* With nothing typed, the list holds the files you opened lately in this pane, latest first (the one on screen left out), then the other open tabs. **Ctrl+P, Enter** goes back to the file before.
* Type part of a name: the pane’s files are found as in the Files search, the ones you opened lately first. Enter opens the file in the Files panel; Ctrl+Enter (Cmd+Enter) opens it in a tab that stays. Enter pressed before the list arrives opens the first file found.
* The files opened lately are kept on this device for each pane: at most 20, for 14 days, removed when you log out. A sensitive file you chose to show is not kept in the list.
## Commands
[Section titled “Commands”](#commands)
Every command shows its group and its key, if it has one. One that cannot run now (no agent in the pane, a view-only device, nothing open) stays in the list, dimmed, with the reason under it, and Enter does nothing.
The commands of the pane’s agent are listed too: `/name` for Claude Code, `$name` for a Codex skill. Picked, the command goes in front of what you already typed in the Chat box, which then has the cursor; nothing is sent. Where the agent has no Chat box but runs in the terminal, the command is typed there, without Enter. A view-only device cannot use them.
## Keys in the Terminal
[Section titled “Keys in the Terminal”](#keys-in-the-terminal)
While the terminal has focus, its keys stay the program’s: Ctrl+P on Linux and Windows goes to the pane (in bash, the previous command). Only Ctrl+Shift+P opens the palette from there, and on a Mac Cmd+P and Cmd+Shift+P, which the terminal never uses. A key you set can also work in the terminal only when it is one the terminal never uses: Ctrl+Shift with a letter other than C and V, or on a Mac Cmd with a letter, a digit or a punctuation key.
## Keys on the Session Tabs
[Section titled “Keys on the Session Tabs”](#keys-on-the-session-tabs)
With a tab of the header’s tab bar focused:
| Keys | Does |
| -------------------------- | ----------------------------------------------------- |
| ← and →, Home and End | Move the focus between tabs |
| Enter or Space | Switch to the tab |
| F2 | Rename the tab in place (Enter saves, Escape cancels) |
| Alt+← and Alt+→ | Move the tab left or right |
| N | A new session, after its name and icon |
| Delete | Close the tab, after the confirmation |
| Shift+F10, or the menu key | Open the tab’s menu, as a right click does |
These keys are fixed, not listed in **Settings > Shortcuts**. A view-only device has only the first two rows.
## Changing Keys
[Section titled “Changing Keys”](#changing-keys)
**Settings > Shortcuts** lists every command with its key:
* **Change**, then press the new keys; Escape gives up. A key needs Ctrl (Cmd on a Mac) or is one of F1 to F12, so typing is never taken over.
* **Remove** leaves the command without a key; **Reset** gives it its default back, and **Reset all shortcuts** every command.
* **Also in the terminal** lets the key work while the terminal has focus, for the keys above.
* A key two commands share is marked on both rows, and neither runs until you change one (Show all commands always runs, so the palette stays reachable). Your own key takes over from a default: that row says which command took it.
* Refused: the keys the browser keeps (Ctrl+W, Ctrl+T, Ctrl+N, Ctrl+Q, Ctrl+Shift+T, Ctrl+Shift+N, Ctrl+Shift+W, Ctrl+Tab), the editor’s Ctrl+S, the terminal’s Ctrl+Shift+C and Ctrl+Shift+V, and the text keys Ctrl+A, C, V, X and Z.
The keys are kept in this browser, for every server it opens, and stay after you log out.
Caution
A browser can take a key before the page sees it. In Firefox, Ctrl+Shift+P opens a private window unless Termote is installed as an app: use the header button, or give **Show all commands** another key.
# Machines
> Watch and drive the coding agents of other computers from one Termote over SSH: add a machine, open its sessions, check its status and remove it.
One Termote can reach the Termote of other computers you own over SSH. They show up under **Machines** in the sidebar, each with its status and the agents running on it. Open one and the whole app works on that computer: its terminals, the [Chat view](/usage/agent-chat/), Files and Changes read and write its own disk. You keep one address and one sign-in; the other computers need no port of their own, only SSH.
Machines are added and removed only from the terminal of the computer running the main Termote. The app picks a machine but never sees its host name, user or key.
## Before you start
[Section titled “Before you start”](#before-you-start)
* On the other computer: Termote installed and started natively (`termote start`); on Linux and macOS, `add` offers to do both for you. A Termote in container mode cannot be reached.
* On this computer: the OpenSSH client (`ssh` and `ssh-keygen`), and an SSH login to the other computer that works from your terminal (a `Host` in `~/.ssh/config` or a host name). Both computers on the same Termote release.
## Add a machine
[Section titled “Add a machine”](#add-a-machine)
```bash
termote machine add pi # an alias of ~/.ssh/config, or a host name
termote machine add pi --label "Kitchen Pi"
```
`add` does four things:
1. The first time, it makes a key of Termote’s own (`~/.config/termote/ssh/id_ed25519`), without a passphrase: the server uses it unattended.
2. It connects with your own SSH login. If your own `known_hosts` already holds the computer’s host key, Termote copies it into its own `known_hosts` (apart from yours), so that key is checked and a different one refused; otherwise OpenSSH asks you to confirm the key, and it is saved there.
3. It asks Termote on that computer where it is installed. On Linux and macOS, if Termote is not there, it offers to install this computer’s release with the online installer (which needs `curl` there) and to start it, showing what `termote start` prints there, its new password included; if Termote there is older than this one, it offers to update it (`termote update --version`, which restarts it); one too old to have machines at all has to be updated before it can be added.
4. After you confirm, it appends one line to `~/.ssh/authorized_keys` there: Termote’s key, allowed to run nothing but `termote bridge` (a forced command with `restrict`: no shell, no forwarding, no TTY).
It then connects the way the server will (Termote’s key, no prompts) and saves the machine only if that works. `--yes` installs, updates and installs the key without asking, `--no-check` saves it without the last check, `--print-key` only prints the line so you can install it yourself.
Import the machines Herdr knows with `termote machine import --herdr`: it asks about each one, then runs `add` for it.
## Use a machine
[Section titled “Use a machine”](#use-a-machine)
In the sidebar, **Machines** lists **This machine** first, then each machine with a coloured dot and the number of its agents that need you or are working; under a machine that cannot be reached, its status and the command to check it. The open machine’s agents are in **Agents**. Click a machine to open it: the page reloads on it (its address gets `?machine=<id>`), the title shows its name, and everything you do happens there. Under **Machines**, a heading such as **On This machine** names the machine the list below belongs to. Click **This machine** to come back. In the command palette, **Switch to the next machine** cycles through them.
What stays on this computer whatever machine is open: signing out, Web Push, [paired devices](/usage/devices/) and signed-in browsers, and the app’s version check. A notification from an agent on another machine opens the app on that machine.
## Status
[Section titled “Status”](#status)
```bash
termote machine list # id, label, alias, when it was added
termote machine status # what the running server sees: online, offline, ...
```
| Status | Meaning | What to do |
| ----------------------- | ----------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------- |
| Online | It answers | |
| Connecting | Not tried yet | Wait a moment |
| Offline | No SSH connection, or no Termote server running there | Check the computer is on, and `termote start` there |
| Key refused | Termote’s key is not installed there | `termote machine remove`, then `add` again |
| Host key changed | The computer’s host key is not the one saved | If you expected it, `remove`, remove the old key from your own `known_hosts` (`ssh-keygen -R <host>`), and `add` again |
| Another Termote release | The two Termotes speak different API versions | Update both to the same release |
After a failure the server waits before trying again (5 seconds, doubling up to 5 minutes); `termote machine status` and the sidebar show why.
## Remove a machine
[Section titled “Remove a machine”](#remove-a-machine)
```bash
termote machine remove pi # by label or id
```
It forgets the machine and its host key, and asks whether to remove Termote’s key line from that computer too (over your SSH login). `termote uninstall --purge` removes the machine list and Termote’s key from this computer; the key lines on the other computers stay until you remove them.
## A Windows machine
[Section titled “A Windows machine”](#a-windows-machine)
Termote on Windows can be a machine too, with OpenSSH Server. Install the key by hand:
```powershell
termote machine add winbox --remote-exe "C:\Users\me\AppData\Local\termote\versions\2.0.0\bin\termote.exe"
```
`add` prints the line to add to the account’s `.ssh\authorized_keys` there, or, for an administrator account, to `C:\ProgramData\ssh\administrators_authorized_keys`. The path names one version, so after `termote update` on that computer, add the machine again.
The forced command and `restrict` were checked with OpenSSH for Windows 10.4: the key gets the bridge’s first line and nothing else, a command sent with it is ignored, and port forwarding and a TTY are refused. To check yours, `ssh -i ~/.config/termote/ssh/id_ed25519 -o IdentitiesOnly=yes winbox` must print `TERMOTE-BRIDGE 1 <version>` and nothing else.
Caution
A domain account on a Windows computer that cannot reach its domain controller (sshd logs `lookup_sid() failed: 1789`) may not be found in the `administrators` group, so sshd never reads `administrators_authorized_keys`: put the line in the account’s `.ssh\authorized_keys` too. Even then the OpenSSH service may end every key-based session right after it accepts the key, before anything runs: that is the service failing to log the account on without a password, not Termote. Use a local account, or connect while the domain is reachable.
## Limits
[Section titled “Limits”](#limits)
* Whoever controls this Termote controls every machine added to it, with the role they have here: a [view-only device](/usage/security/#who-can-do-what) here is view-only there. The key can only reach Termote there, never a shell.
* An alias that goes through a `ProxyCommand` or `ProxyJump` is warned about: the jump host would have to accept Termote’s key, and your agent is never used.
* A Termote in container mode cannot be a machine.
* On Windows as the main computer, every connection is a new SSH login (no connection sharing), so the machines list refreshes less often.
# Agent Overview
> Every coding agent in every session at a glance: context and limits, stuck agents, history, and the dialogs waiting on you answered in one place.
The agent overview shows every pane running a coding agent (Claude Code, Codex, any agent your backend reports), in every session, on one screen. It is for checking on several agents at once: which one waits on you, which is still working, which has finished.
## Open it
[Section titled “Open it”](#open-it)
* **Desktop**: the grid button at the top of the sidebar.
* **Phone**: the view menu in the header, then **Agent overview**.
* **Address**: `#/overview` after your Termote address. A reload keeps it, and **Copy link** copies it.
It covers the panes without closing them: leave with its close button, `Esc`, the browser’s Back button, or by picking a session. Back and Forward move between the overview and the pane you came from.
## The cards
[Section titled “The cards”](#the-cards)
One card per agent, sorted by what needs you first: blocked, then working, done and idle. Within one state, the agent that has been in it longest comes first. Each card shows:
* the agent’s state and how long it has been in it. “≥ 5 min ago” means the server found the agent already in that state when it started watching (after a restart, for example);
* the session and tab it runs in, and **worktree** for a linked git worktree;
* its last message;
* the branch and the number of changed files of its directory;
* how full its context is: a bar, the share of the context window and the tokens, for example `72% 144k/200k`, and the model for Claude Code. Bars turn yellow from 70% and red from 90%.
Tap a card to open the agent: its [Chat view](/usage/agent-chat/) for Claude Code and Codex, else its terminal. A Chat card also has a terminal button. The filter above the cards counts agents and is the overview’s own: it does not change the sidebar’s.
The magnifier in the overview’s header opens a search box over the cards: it keeps the agents whose tab, group, pane, agent or process matches, together with the filter. **Enter** opens the first one, **↓** moves to it, and **Esc** clears the box without leaving the overview. The **Needs you** queue and the activity log are not searched.
Note
With tmux and psmux, only the active pane of each window is watched: an agent in another split of the same window has no card. Herdr and Zellij watch every pane.
## Context and limits
[Section titled “Context and limits”](#context-and-limits)
For Claude Code the window follows the model it runs, as its transcript records it at start and on each `/model` switch: 1M for the models that have it of their own (every model from Claude 5 on, Opus from 4.7) and for a `[1m]` model, 200k for any other. When no model can be read, the window is assumed to be 200k and the share is marked `~`, until the context grows past 200k. For Codex the share is the one Codex reports itself (“% context left”, turned into the share used). For OpenCode the context is the tokens of its last answer, and the window the one OpenCode’s own list of models gives that model; a model the list does not have shows its tokens without a share.
With a Codex pane signed in to a ChatGPT plan, an **Accounts** strip above the cards shows that account’s limits: a bar for the 5-hour and the weekly window, when each resets, and when Codex last recorded them. Numbers older than 30 minutes are dimmed, since the account may have been used elsewhere since. There is no such line for Claude Code: its limits are not written anywhere Termote reads.
The header also shows a small chip for the pane you have open, such as `ctx 72% · 7d 95%` (the limit only for Codex). On a phone it shows only the higher of the two. Tap it to open the overview on that pane’s card. Turn it off in **Settings > Sessions > Agent usage in the header**.
## Tokens and cost
[Section titled “Tokens and cost”](#tokens-and-cost)
Each card of a Claude Code, Codex or OpenCode session shows how many tokens the session has used so far and what they would cost at API list prices, such as `1.2M tokens · ~$3.40`, with its subagents’ share on a line of its own. Above the cards, a strip adds up every session of the server’s user, today and over the last 7 days, one line per agent; the days are your device’s own. The same strip at the top of [Session history](/usage/agent-chat/#resuming-an-old-session) covers the sessions of the pane’s folder, and an old session shows its own total under its title.
The numbers come from the agents’ own files (Claude Code transcripts, Codex rollouts), never from the terminal. Each request counts once, subagents included. The cost is an estimate from the list prices built into Termote, dated in the tooltip: a subscription (Claude Pro or Max, a ChatGPT plan) is not billed by the token, and fast, batch or regional pricing is not counted. A model Termote has no price for shows its tokens only (`no price`), and a total that leaves some out is marked `+`. Turn the cost off in **Settings > Sessions > Estimated cost**; the tokens stay.
OpenCode’s numbers come from its background service, and its cost is the one OpenCode recorded at the prices of the provider you use (your own providers included), not Termote’s list prices, so the two are worked out differently. A model OpenCode records no cost for (a local or free one) shows its tokens only. A session’s total is OpenCode’s own, the request that names the session included; the strip adds up the answers by when they came, so it leaves that request out. A forked OpenCode session starts from 0. When the service is not running, OpenCode adds nothing and the other agents still show.
Limits: the strip counts the files written in the last 8 days, at most 5000 per agent (for OpenCode, the sessions updated in that time, at most 5000), and says so when it left some out. A session forked from another (`--fork-session`) counts the messages it copied in its own total.
## Needs you
[Section titled “Needs you”](#needs-you)
When an agent waits on a dialog (a permission, a question), the overview shows it at the top, one at a time, with the same buttons as the Chat view. The dialog on top stays until you answer it or press **Skip**: an agent that starts waiting later joins the end, never in front of it. After an answer the card says **Answered**, and only **Next** shows the next dialog, so a second tap never answers another agent. **Show again** brings back the dialogs you skipped.
A [view-only device](/usage/devices/#view-only) sees the dialogs without buttons. An agent whose dialogs Termote cannot read or answer gets an **Open terminal** button instead. With Zellij, if the answer cannot reach the pane at that moment, the card says so and you can try again.
## History and activity
[Section titled “History and activity”](#history-and-activity)
**History** at the bottom of a card opens its last hour as a strip, one colour and mark per state (blocked is striped, idle hollow), with the minutes of each underneath. The part before the first change the server recorded stays blank: it is unknown, not idle. For Claude Code it also lists the session’s subagents: each one’s type, description, state, the last tool it called, how long it ran or has been running and the tokens of its last request (for OpenCode too, its subagents being sessions of their own). One started by another is indented under it. “(guessed)” marks a subagent taken as done because its file has not changed for 10 minutes, when the transcript cannot tell. Closed, the button still says “2 subagents, 1 running”.
**Activity** in the overview’s header lists every change of state of every agent since the server started (the last 500), newest first. Filter it by the state changed to. A row opens that agent’s pane, even after its tmux window moved; a row of a pane that has closed cannot be opened.
## Stuck agents
[Section titled “Stuck agents”](#stuck-agents)
An agent that says it is working but has written nothing for a while gets an hourglass line: “May be stuck: no new activity for 14 min”. Set how long in **Settings > Sessions > Stuck agent** (3 to 60 minutes, 10 by default). Writes of its running subagents count as activity. It is only a hint on the card: no notification is sent.
Caution
The times come from the server. They start when the server first sees an agent, and they are recorded only while a page is open or a device has [notifications](/usage/settings/#notify-when-an-agent-needs-me) on.
# Prompt Templates
> Save the prompts you type again and again as templates with variables, insert them into the Chat, and export or import them as a JSON file.
A prompt template is a name and a text you save once and insert into the [Chat view](/usage/agent-chat/) whenever you need it: a review request, a bug report, a release check. The text can hold variables that are filled in when you insert it. Templates are kept on the server, so every device that signs in to it sees the same ones.
## Making a Template
[Section titled “Making a Template”](#making-a-template)
Open **Settings > Prompt templates** and press **New template**. Give it a name (up to 64 characters, unique whatever the case) and a text (up to 16 KB, the most a Chat message can be). The buttons under the text put a variable where the cursor is. **Edit** and **Delete** on each row change or remove one.
A template cannot hold control characters (other than tab and new line) or characters that show nothing, such as zero-width spaces or bidi marks: a template you share must not carry an instruction its reader cannot see. The editor says which character it found and where. A server keeps up to 100 templates.
If another device changed the template while you were editing it, saving says so: **Load theirs** takes their version, **Keep mine** saves yours over it. If it was deleted meanwhile, **Save as new** saves yours as a new template.
## Variables
[Section titled “Variables”](#variables)
| Variable | Filled with |
| --------------- | ---------------------------------------------------------------------------------------- |
| `{{file}}` | The path of the file the pane’s Files view shows, relative to its folder |
| `{{selection}}` | The text you selected last for the pane: in the terminal, a file, the editor or the Chat |
| `{{branch}}` | The git branch of the pane’s folder, or of its worktree group |
| `{{input}}` | Nothing: you type it when you insert the template |
Each variable is filled once, however many times it appears. Anything else between `{{` and `}}` stays as it is. A selection is kept in the page’s memory only, never saved, and is not taken from a sensitive file you chose to show.
Note
With tmux, the terminal shows the whole window: text selected in another split of it counts for the pane on screen. Check the selection in the dialog before you insert it.
## Inserting a Template
[Section titled “Inserting a Template”](#inserting-a-template)
Open the agent’s Chat, then pick a template in any of three ways:
* **The Templates button** next to the message box: search by any word of a name.
* **The `/` list**: templates come after the agent’s commands, under **Templates**. They are never picked by Enter alone: move to one with the arrow keys, or tap it. With no command to pick, Enter does what it always does.
* **The command palette** on a computer (`>`): templates are listed with the commands.
A template with variables opens a dialog first. **Your text** (`{{input}}`) has the focus; the other fields come filled in, each saying where its value came from, and you can change any of them. The preview shows the result. **Insert** (or Ctrl+Enter, Cmd+Enter on a Mac) puts the text in the message box where the cursor was, or in place of the `/name` you typed. A template without variables goes in at once.
Caution
Nothing is ever sent for you: the text waits in the message box until you read it and send it.
## Export and Import
[Section titled “Export and Import”](#export-and-import)
**Export** saves every template as a JSON file (`termote-templates-YYYY-MM-DD.json`): names and texts only. **Import…** reads such a file (up to 4 MB) and lists, before anything changes, which templates are new and which replace one of the same name. An import is all or nothing, and it cannot be undone: export first to keep a copy.
## Who Can Change Them
[Section titled “Who Can Change Them”](#who-can-change-them)
A device with full access makes, changes, deletes and imports templates. A [view-only device](/usage/devices/) lists and exports them, and has no message box to insert them into. If a newer version of Termote saved the templates, an older one shows them but cannot change them until it is updated.
The templates are kept in the server’s state folder: `~/.local/state/termote/templates/templates.json` on Linux and macOS (under `$XDG_STATE_HOME` when it is set), `%LOCALAPPDATA%\termote\state\templates` on Windows, and the `termote-state-<uid>` volume in a container. A native server and a container on the same computer each keep their own, and they are not shared between servers. `termote uninstall --purge` deletes them with the rest of the state folder: export them first if you want to keep them.
# Security Model
> What Termote protects and what it cannot: who can do what, what the server enforces, how to expose it safely, and what to do when a device is lost.
What Termote protects, what it does not, and what to do when a device is lost. Pairing itself is on [Pair a Device](/usage/devices/).
## Termote is a remote shell, by design
[Section titled “Termote is a remote shell, by design”](#termote-is-a-remote-shell-by-design)
Whoever signs in with full access has the rights of the OS user that runs the server: they run commands, read and change files, read the saved password (`termote show-password`), change the configuration (`termote start --no-auth`) and pair more devices, from the interface or by typing `termote pair` in a terminal. A device paired from a terminal shows as “Paired with the password, kept when a browser signs out”, like one paired with the password from the CLI. One paired from a browser signed in with the password shows as “Paired from a signed-in browser” (`termote devices`: `a signed-in browser (<id>)`).
No setting turns a full-access client into something less. The real boundary is the OS user: to limit what Termote can reach, run it as another user with less access.
## Who can do what
[Section titled “Who can do what”](#who-can-do-what)
| Signed in as | Can |
| ------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| The password | Everything (full). |
| A full device | Everything the password can, except pairing another full device (it pairs view-only devices only) and signing a password browser out (it only lists them). |
| A view-only device | Watch the terminal, read the Chat view and Files/Changes inside a git repository (not the home dir, only the files git tracks). It types nothing and changes nothing, but reads everything printed in the terminal; its Chat view shows the conversation’s text, no tool call or command output. |
| Anyone, with `--no-auth` | Everything, as soon as they reach the port. |
## What the server enforces
[Section titled “What the server enforces”](#what-the-server-enforces)
* It listens on `127.0.0.1` unless started with `--lan`.
* On Linux and Windows (native), a connection made from this machine (any loopback address, or the machine’s own LAN address with `--lan`) is closed before its first byte is read unless the process at the other end runs as the server’s OS user, root/SYSTEM, or a user named with `--allow-local-user` (a reverse proxy). A connection whose owner cannot be found, or that is no longer established, is refused too. On by default; refusals leave `local-user-refused` audit lines (at most one per user every 10 s).
* The CLI reaches the native server over a socket only its own OS user can open (a `0600` Unix socket in a `0700` dir, or a named pipe whose ACL names that user and SYSTEM), after checking the server end runs as that user or root/SYSTEM (on Windows, that the pipe is owned by that user or SYSTEM). It sends no password there; `termote status` says `Via: socket`. Without the socket (the container, an older server) it falls back to TCP and the password, except to the native server on Windows, which gets the password over its pipe only.
* Settings > Signed-in browsers and `termote signins` list the browsers signed in with the password (the sign-in form, Basic auth, a CLI link); each can be signed out, or every one but the current. A sign-out closes that browser’s open terminals, one still opening included, drops the pairing codes it made, and revokes the devices it paired (and the devices those paired). Logging out of a browser only drops its codes. A full device sees the list but signs no one out.
* `termote url --open` (and the Herdr panel) opens the app already signed in: the CLI asks the server for a single-use token (30 s), writes it into an HTML page only its user can read (`0600` in a `0700` dir; an owner-only ACL on Windows), and opens that page, which posts the token. The token is never in a command line, a URL, the output or the history; only a client signed in with the password itself (Basic auth, as the CLI) or over the local socket gets one, never a session cookie or a device.
* Every response, a refusal for an unknown host included, also carries `Cross-Origin-Opener-Policy`, `Cross-Origin-Resource-Policy` and a `Permissions-Policy` that turns off camera, microphone, location and the other device features the app never uses.
* Requests naming an unknown host are refused; writes and the terminal stream must come from the Termote page itself, and the stream needs a single-use token.
* Failed sign-ins and wrong pairing codes are rate limited.
* A view-only device is refused every write on the server, not only hidden buttons in the interface.
* With Zellij (`--mux zellij`), what you type and Chat messages reach a pane only through the terminal of a Termote Zellij client, never through process arguments, which other local users could read. A view-only device watches a Zellij pane through `zellij subscribe`, which cannot type.
* With herdr-core (`--mux hc`), which has no login of its own, Termote runs only natively, never in a container, and `--no-auth` needs `--allow-hc-no-auth`. Termote calls only a fixed list of core methods, on this machine’s terminals that the herdr app shows, never changes the app’s layout, and writes only the label that marks an agent seen. A view-only device watches through an observer, which the core lets neither type nor resize.
* A Content-Security-Policy keeps the page from loading or running code from elsewhere.
* A paired device can carry a time limit (30 days by default when pairing): past it, the device is refused and its open terminals close. A device paired by one with a limit never outlasts it.
* Revoking a device also revokes every device it paired. Logging out on a device revokes only that device.
* Only the password (a password sign-in, or `termote pair` on the server) pairs a full device.
* Pairing, revoking a device or a sign-in, a device reaching its limit, signing in on the sign-in form or with a CLI link, and a refused local connection each leave an `audit:` line in the server log.
## What it cannot prevent
[Section titled “What it cannot prevent”](#what-it-cannot-prevent)
* **A full device used on purpose.** It has a shell: it can read the password, pair devices from a terminal, edit the devices file or turn sign-in off. The limits above stop mistakes and clicks in the interface, not someone who means it.
* **Secrets printed in a terminal.** A view-only device reads whatever an agent or a command printed in the terminal. Its Chat view shows the conversation’s words only, no tool call and no command output, but what the agent writes in its reply, and an open dialog’s card, which shows what the terminal shows. A machine running an older Termote still shows it the tool results of that machine.
* **Other services on the same host name.** A cookie is not bound to a port: every service on that name receives Termote’s cookies. Give Termote a host name of its own.
* **Programs of the same OS user.** Anything else running as that user reads the same files Termote does.
* **Plain HTTP.** Without HTTPS (Tailscale, a Cloudflare Tunnel, or a proxy with a certificate), the password and cookies cross the network in clear text.
* **The audit lines are not proof.** `termote logs clean`, or any full device, deletes them. Behind a proxy (`tailscale serve`) the address they name is the proxy’s, loopback.
* **Browser extensions.** An extension allowed on the page reads it and types into it as you. Termote cannot tell its input from yours.
* **Port forwarding tools.** VS Code Remote and Dev Tunnels, ngrok or Cloudflare Tunnel forward the port from a process of your own user: whoever reaches the tunnel reaches Termote, with only the password in the way. [`--cloudflare`](/installation/cloudflare/) (or `--proxy-client-header` for ngrok) makes failed sign-ins count per client, so one stranger’s wrong guesses do not lock out everyone else, but only Cloudflare Access (or the tunnel’s own sign-in) keeps strangers away from the sign-in page. Termote warns when a Cloudflare hostname answers without Access.
* **Proxies running as root or SYSTEM.** `tailscale serve`, `docker-proxy` and other root proxies are trusted (root reads the saved config anyway), so every local user can go through them. On Windows the same holds for a running service whose process the server’s user can open but whose token it cannot read: the process the Service Control Manager runs, not one it starts in turn (a proxy under NSSM or WinSW is refused) and not a per-user service. Services that fetch a URL for whoever asks, such as BITS and WebClient (WebDAV), are among them: any local user can send a request through them. `tailscale serve` is not: a standard user cannot open the `tailscaled` process, so its connections are refused.
* **Which process a Windows socket belongs to.** On Windows, who owns a loopback connection, and who listens on the container’s port before the CLI sends it the password, is read from the PID the system’s TCP table names, checked against that process’s start time. Microsoft does not document which process that PID is for a socket handed from one process to another before its bind, so such a socket is not proven to be caught. The native server’s CLI never sends the password over TCP (only over its pipe), but a sign-in link’s single-use token, which it gets over the pipe, reaches the server over TCP from the browser: the CLI checks the port’s listener by that PID before asking for it. While the server listens, Windows does not let another account bind its port on the same address, nor on any address with `--lan`; what is left is the token’s 30 s, in which the server would have to stop and another user’s listener take the port.
* **`--no-auth` on Windows.** With `--no-auth` on the native server, that check of who owns a loopback connection is all that stands between another user signed in to the same machine and the server. If a connection passed it, that user would get a shell as you. Keep the password on a Windows machine anyone else signs in to.
* **The system clock on Windows.** That check compares two readings of the wall clock: setting the clock back (an administrator or the time service) at the wrong moment can let it pass.
* **WSL 2.** Every Windows user reaches a server in WSL 2: in NAT mode through a relay running as root, in mirrored mode through a loopback whose client end WSL cannot see, which is let through.
* **macOS.** It does not check which local user makes a connection (that takes `lsof` for each one): another user of the Mac has only the password in the way.
* **Another local user slowing the server down.** Looking up who owns a connection reads the machine’s socket tables once for all the connections waiting, so a flood of connections from another user does not multiply that work. It can still slow the server through other means (using up ports, CPU); Termote does not promise to stop a local denial of service.
* **The container and `termote serve` configured from the environment.** Neither checks the local user of a connection, and the container has no CLI socket.
* **Sessions live in memory.** Restarting the server signs every browser out (devices stay paired).
* **A sign-out ends the cookie, not the password.** A client that sends the password with every request (Basic auth) is signed in again at once; change the password for that.
* **What the app keeps on the device.** So that closing the app loses nothing, it keeps in the browser’s storage the files open in Files and Changes and the unsaved text of files being edited, never a file shown as sensitive. Log out and Clear cache & reload remove it, in every tab (unsaved text included). A device revoked from elsewhere, or whose session ended, still holds it until the app is opened there again or 14 days pass: a file not marked sensitive can still hold a secret.
* **Two servers on one state dir** (the service and a `termote serve` started by hand). A revoke in one does not close the terminals open in the other; the other refuses the device from its next request. Writes to the paired devices take a lock first, so neither brings back a device the other revoked (an older release running beside it does not take the lock).
## Files and Changes
[Section titled “Files and Changes”](#files-and-changes)
What the [Files](/usage/files/) and [Changes](/usage/changes/) views refuse to show, and how they run git.
### Files That May Contain Secrets
[Section titled “Files That May Contain Secrets”](#files-that-may-contain-secrets)
Files whose names usually mean secrets are listed with a lock and the word `Sensitive`: for example `.env`, `.env.*`, `*.env` and `.envrc`, private keys (`*.pem`, `*.key`, `*.jks`, `id_rsa*`, `id_ed25519*`), `.netrc`, `.npmrc`, `.pgpass`, `.my.cnf`, `.vault-token`, `.git-credentials`, `credentials*`, `auth.json`, `secrets.*`, `.dev.vars`, Terraform state and variables (`*.tfstate`, `*.tfvars`) and anything under `.ssh`, `.gnupg`, `.aws`, `.kube`, `.docker`, `.config/gh` or `.config/gcloud`. A link to such a file counts too, as does a rename from or to one.
Opening one asks `This file may contain secrets. Show its contents?` first, for the file and for its diff. The answer is not remembered: the next time, it asks again. The server checks this itself: without the confirmation it sends no contents at all.
Caution
The lock is a warning based on names, not a guarantee. A secret in a file with an ordinary name (`prod-creds.yaml`) is shown without asking. Anyone who can sign in to Termote can read every file under the pane’s directory.
### Never Shown
[Section titled “Never Shown”](#never-shown)
Some paths are never served (or saved, created in, deleted, or found by name), even after confirming:
* Termote’s own config and state directories (the saved password, logs) and its trash
* `/proc`, `/sys` and `/dev` (process environments and devices)
* the repository’s `.git` directory, which is not listed in Files either
Paths are checked on the server against the root: `..`, absolute paths and links that lead outside the root are refused, except a link into another worktree of the same repository, which is read-only, never served to a view-only device and still refused inside a `.git` or one of the directories above.
### How git Is Run
[Section titled “How git Is Run”](#how-git-is-run)
Termote runs `git status` and `git diff` itself to fill Changes, `git rev-parse`, `git log` and `git diff-tree` for the last commit (of `HEAD` only, never a commit named by the browser), `git blame` for the blame column (from `HEAD`, given the text shown on its input), and `git cat-file` to read the before and after versions of an image. Stage and Unstage are the only commands that write, and they write the index only (`git add`, `git restore --staged`, `git reset`, `git apply --cached`); the patch of a hunk is built by Termote, never taken from the browser. A repository’s own settings cannot make it run a program: file system monitors, filter drivers, external diff tools and text conversion are turned off for these commands, and submodules are skipped. At most two git commands run at a time, each limited to 10 seconds (30 seconds for one that writes the index).
## Recommendations
[Section titled “Recommendations”](#recommendations)
* Use Termote from the installed PWA, or a browser profile without extensions.
* A reverse proxy running as another OS user needs `termote start --allow-local-user <name>`; until then its connections are refused and the log says so.
* Look at Settings > Signed-in browsers (or `termote signins`) from time to time.
* Use HTTPS: `termote start --tailscale <host>` (see [Tailscale](/installation/tailscale/)), or a proxy with a certificate.
* Give Termote a host name no other service uses.
* Pair a device view-only unless it must type.
* Keep the default 30-day limit; choose Never only for your own computers.
* Look at `termote devices` from time to time, and revoke what you no longer use.
* Read the `audit:` lines with `termote logs` (native) or `termote container logs` (container).
## If a device is lost
[Section titled “If a device is lost”](#if-a-device-is-lost)
* **A browser signed in with the password:** sign it out in Settings > Signed-in browsers, or `termote signins` then `termote signins revoke <id>`. That also revokes every device it paired. If the browser is no longer listed (the server restarted, it logged out), find its id in `termote devices` (`a signed-in browser (<id>)`) or Settings > Devices, and `termote signins revoke <id>` still revokes those devices; if one was full, check what it could have left behind, as below. A device a browser paired before 2.0.1 is listed as paired with the password instead, and signing that browser out leaves it: revoke it by hand in Settings > Devices or with `termote devices revoke`. If the password itself may be known (saved in that browser for Basic auth, written down), run `termote start --fresh` instead.
* **Unsaved text of files.** A full device keeps the files it had open and their unsaved text in its browser storage (never a file shown as sensitive), for 14 days after the last edit. Revoking it does not remove them: if you get the device back, open the app there and Log out, or clear the site’s data in the browser.
* **View-only:** revoke it in Settings > Devices or with `termote devices revoke <id>`.
* **Full:** revoke it, then run `termote start --fresh`: it sets a new password, which signs out every device and every session. A full device had a shell, so also check what it could have left behind: `~/.ssh/authorized_keys`, `crontab -l`, and the service units and login scripts of your user.
## Reporting a vulnerability
[Section titled “Reporting a vulnerability”](#reporting-a-vulnerability)
Report it privately through [GitHub Security Advisories](https://github.com/lamngockhuong/termote/security/advisories/new), not in a public issue.
# Session Management
> Manage terminal sessions with tmux, Herdr, Zellij or herdr-core in Termote: add, rename, switch and close sessions, deep links and troubleshooting.
Termote models terminals as three levels — **group → tab → pane** — regardless of backend. With the default tmux backend a group is a tmux session, a tab is a tmux window and a pane is a pane of that window.
With tmux, every session on the tmux server appears as a group, including sessions you started yourself outside Termote (`tmux new -s work`): anyone signed in to Termote can switch to them and type into them. Termote’s own session (`main` unless `TMUX_SESSION` says otherwise) is always listed first. Each device remembers which session it shows; within a session, every device attached to it shares the current window.

## Default Session
[Section titled “Default Session”](#default-session)
Fresh installs start with a single **Shell** session (group). Create additional groups as needed for different workflows (e.g., Claude Code, Copilot, builds).
## Managing Sessions
[Section titled “Managing Sessions”](#managing-sessions)
### Desktop
[Section titled “Desktop”](#desktop)
* **Switch**: Click session in left sidebar or in the **session tabs** in the header row (if enabled)
* **Edit**: Double-click session name, or hover and click pencil icon
* **Remove**: Hover and click X icon (requires 2+ sessions), then confirm with “Close session”
* **Touch screen (iPad)**: With no hover to show the icons, swipe a session in the sidebar to the right to edit it, or to the left to remove it, as on a phone
* **Add**: Click “New session” in the sidebar, or the “+” after the session tabs. In the [compact layout](#compact-layout) the “+” first asks for a name and an icon
* **A tab’s menu**: Right-click a session tab, or press **Shift+F10** (or the menu key) while it has focus: **Rename** (**F2**, edited in place: Enter saves, Escape cancels), **Edit icon**, **Move left** and **Move right** (**Alt+←** and **Alt+→**), **New session…** (**N**, a name and an icon) and **Close** (**Delete**, with the same confirmation as the X). Each item shows its key on the right. Rename is dimmed, saying why, where the backend keeps the tab’s name, and the moves only appear where the backend can reorder tabs. A view-only device gets no menu
* **A tab’s hover**: Besides the name and description, it lists what runs in the tab and, for a tab with one pane, that pane’s name and metadata
* **Collapse sidebar**: Click the collapse icon (top-right of sidebar) to show icons only — maximizes terminal width
* **Sidebar width**: Drag the sidebar’s right edge to widen it when a name is cut short, or to narrow it (**←**, **→**, **Home** and **End** with the edge focused, a double-click resets it). The width stays on this device.
* **Fullscreen**: Click the fullscreen button in the header to enter fullscreen mode (not shown where the browser cannot go fullscreen, such as iPhone)
* **Session tabs**: Browser-like tabs that share the header row, for quick tab switching on desktop. Can be toggled via Settings → Sessions → “Show session tabs”
* **Compact layout**: One line per workspace instead of every tab, for many projects at once. See [Compact Layout](#compact-layout)
### Mobile
[Section titled “Mobile”](#mobile)
* **Switch**: Tap the session chip in the header (“Open sessions menu”) to open the sessions list as a bottom sheet, then tap a session. The sheet opens scrolled to the current session
* **Current session**: The top of the sheet shows the current session with **Edit** and **Delete** buttons, so neither needs a scroll or a swipe. Delete asks for confirmation and is hidden when only one session is left
* **Edit**: Swipe left on session, tap Edit
* **Remove**: Swipe left on session, tap Remove, then confirm with “Close session”
* **Add**: Tap “New session” in the header of the sessions sheet; once it is created, the sheet closes on the new session
* **Sessions waiting on you**: When another session has an agent waiting on you, the session chip shows how many, in red, in place of its arrow
### Compact Layout
[Section titled “Compact Layout”](#compact-layout)
With many workspaces open, the sidebar’s list of every tab gets long. The compact layout gives each workspace one line and moves its tabs to the header. Turn it on in Settings → Sessions → **Sidebar layout** → **Compact** (desktop only; a phone keeps its sessions sheet).
* **A workspace’s line**: a coloured letter, its name, the first chip of its [metadata](#naming-a-pane) (then **+N**), its branch on the right when known, and the badge of its agents. Its tab count and its menu (rename, move, worktrees, close) show on hover or focus; double-click the line to rename it.
* **Picking a workspace** opens the tab you last looked at in it on this device, or else the tab the backend has current. Its tabs are in the header’s tab bar, which the compact layout always shows, with the [tab menu](#desktop) for everything the sidebar’s tabs offer in the detailed layout. The “+” there asks for the new tab’s name and icon.
* **Worktrees** sit indented under their repository’s checkout, named by their branch. The chevron beside the checkout hides or shows them; hidden, the checkout’s badge speaks for them too, and the checkout is marked as the current line when the tab on screen is in one of them.
* **Moving**: drag a line, or press **Alt+↑** and **Alt+↓** with it focused, as in the detailed layout.
* **Search** keeps the workspaces with a match, and **Enter** or a click opens the first match. The [agent filter](#filtering-by-agent-status) is hidden; the one you saved stays for the detailed layout.
* **Agents**: under the list, every pane running a coding agent, in every workspace, grouped as **Needs you**, **Working**, **Done** and **Idle**. A line names the workspace and the tab and how long the agent has been in its state; the pane on screen is marked. Click a line to open the pane, in the Chat view when the agent has one, as an [overview](/usage/overview/) card does. Drag the line above the section to change its height (**↑**, **↓**, **Home** and **End** with it focused, a double-click resets it), and click its title to fold it. Both stay on this device. The section is not narrowed by the search and is hidden while no agent runs.
### What runs in a session
[Section titled “What runs in a session”](#what-runs-in-a-session)
Each session in the sidebar and in the mobile sheet shows the commands running in its panes under its name (`bash, vim`), and the Herdr pane strip shows each pane’s command under the pane’s name. Only the first word of each command’s name is shown (`npm`, never `npm run dev`), never its arguments. The confirmations to close a session, a pane or a group add what will be stopped (`Running: bash, vim.`), as the last poll saw it, so a command started a few seconds earlier may be missing. An idle pane shows its shell (`bash`).
### Groups: tmux sessions, Herdr workspaces and Zellij sessions
[Section titled “Groups: tmux sessions, Herdr workspaces and Zellij sessions”](#groups-tmux-sessions-herdr-workspaces-and-zellij-sessions)
When the server can manage groups, every group has a header with a **⋯** menu, even a single tmux session:
* **New**: **New tmux session** (tmux), **New workspace** (Herdr) or **New Zellij session** (Zellij), under **New session** in the sidebar or at the top of the mobile sheet, asks for a name and a directory: an absolute path on the host, `~/dir` for a directory in your home, or empty for your home directory. Its first tab opens once it is created. A name already taken, a relative or missing directory, or a file is refused with the reason, and what you typed stays.
* **Recent**: above the name, the dialog lists the groups created last with a directory (up to 20, five until **Show more**), the same list on every device. A tap fills in the name and the directory; you still press **Create**. A name an open group uses becomes `name-2`, `name-3`, … The ✕ beside an entry removes it on every device. A group made in the home directory is not listed, and a view-only device does not see the list.
* **Rename**: in the **⋯** menu; the name is edited in place. Termote’s own tmux session (`main`) and the Zellij session `main` cannot be renamed.
* **herdr-core**: a group is a space of the herdr app. The app names a space after its folder, so **New workspace** asks only for the directory. Once the herdr app has run on this machine, a space can be renamed in the **⋯** menu, and the herdr app shows the new name.
* **Close**: in the **⋯** menu; it asks first and says how many tabs end with it. Closing Termote’s own tmux session starts a new, empty one right away, and every device viewing it reconnects to that (the same holds for the Zellij session `main`). Closing a Zellij session deletes it: Zellij cannot resurrect it. A Herdr workspace with worktrees is closed in Herdr, or after removing them.
The directory check only catches mistakes: the shell that opens can still `cd` anywhere. On Windows, the directory must be on a drive letter (`C:\work`), and psmux names and directories cannot contain `#`. A Zellij session name cannot contain `/`, start with `-`, be `help` or `options`, or be too long for Zellij’s socket path.
### Split windows (tmux)
[Section titled “Split windows (tmux)”](#split-windows-tmux)
A tmux window split into several panes shows a **pane strip** below the tab bar: one button per pane, with the command it runs and its agent badge. The terminal always shows the whole window, so picking a pane gives it tmux’s focus: what you type goes there, and the Chat and Files views follow it. That focus is tmux’s own and shared with every client: the tmux client on your desktop moves too, and when the focus changes on the desktop, Termote follows at its next poll. On a view-only device the strip only shows the panes. To close a pane, click the X next to its name and confirm; a window’s last pane is closed by closing the tab. psmux (Windows) shows one pane per window.
### Pane layout
[Section titled “Pane layout”](#pane-layout)
The **Pane** menu in the header (the split icon) changes the layout of the tab on screen, acting on the pane shown:
* **Split right** and **Split down** open a new pane beside or under it, in its directory.
* **Zoom** makes the pane fill its tab; choose it again to bring the other panes back.
* **Swap with next pane** trades places with the next pane of the pane strip.
* **Resize left/right/up/down** (desktop only) moves the pane’s edge by 5 cells. The menu stays open, so you can click again.
* **Close pane** closes the pane shown after a confirmation, once the tab has another pane.
Split, zoom, swap and resize are also in the command palette, under **Panes**, with **Previous pane** and **Next pane**, which you can give a key in Settings → Shortcuts. The menu is not shown on a view-only device, nor on a backend without these actions: psmux (Windows), Herdr older than 0.9.3 or on Windows, and Zellij 0.45.0. The palette still lists the commands there, greyed out with the reason.
The backends differ in a few ways:
* **tmux**: a split leaves the focus where it was, so the pane shown stays the same. Zoom gives the zoomed pane tmux’s focus, for every client, your desktop’s included.
* **Herdr**: on a computer, a tab of 2 to 4 panes shows its panes side by side, as the Herdr desktop arranges them (see [Herdr Backend](#herdr-backend)), and the menu offers everything. Where Termote shows one pane at a time (the pane strip: a phone, more panes, the setting off), zooming, swapping and resizing would change only the Herdr desktop, so the menu offers **Split right**, **Split down** and **Close pane** alone, and the palette greys the others out (“Changes only the Herdr desktop”). While the tab is zoomed, **Zoom** ends the zoom. After a split, Termote works on the new pane; Herdr’s own focus stays put.
* **Zellij**: after a split, Termote shows the new pane. Swap turns the panes of the tab around one place, so the pane strip keeps its order while the panes move on screen. Zoom moves every client viewing the tab, your own Zellij included, to the zoomed pane. Through the API, a split into a directory other than the pane’s own is refused (Zellij ignores it).
* **herdr-core**: as on Herdr, a tab of 2 to 4 panes shows side by side on a computer, as the herdr app arranges them. Only **Split right**, **Split down** and **Close pane** are offered: the app places a new pane after the tab’s last one, and zoom, swap and resize stay in the app.
### Naming a pane
[Section titled “Naming a pane”](#naming-a-pane)
Two panes running the same agent look alike. **Pane → Rename pane** gives the pane shown a name of up to 64 bytes; leave the field empty to remove it. The name is kept by the backend, not on the device, so it is still there after a reload and on your other devices. It shows instead of the agent’s name or the pane title in the pane strip, under the tab’s name in the sidebar for a tab of one pane, in the Chat view’s header and in notifications (“api · work / build”), so two Claude Code panes no longer read the same. A name cannot start with `-`, end with `;` or hold control characters.
Rename pane is offered on tmux 3.0 or later, Herdr 0.9.3 or later and Zellij 0.45.1 or later, also where the layout actions are not (Herdr on Windows), and never on psmux or a view-only device. How each backend keeps it:
* **tmux**: in the pane option `@termote-name`, not the pane title, which the program in the pane overwrites. The tmux desktop does not show it.
* **Herdr**: as the pane’s label, which the Herdr desktop shows too.
* **Zellij**: as the pane’s title, which Zellij shows. Termote remembers the names it set while it runs; after Termote restarts, after the session is renamed, or once the program in the pane sets its own title, the pane shows its title again.
**Herdr metadata.** Herdr plugins and tools can report short tokens for a workspace or a pane (a PR number, an environment). Termote shows them read-only as small faint chips with a dashed border beside the workspace’s name, the pane in the pane strip, or the row of a one-pane tab: three at most (one on a workspace’s line in the compact layout), then `+N`, and the tooltip lists them all and says they come from a Herdr plugin. They are always plain text, never a link, so a plugin cannot make one look like a Termote message. The server keeps at most 8 tokens of up to 64 characters each, and a view-only device gets none.
### Reordering tabs and workspaces
[Section titled “Reordering tabs and workspaces”](#reordering-tabs-and-workspaces)
Tabs keep the order they were made in until you move them, and Termote shows the order the backend keeps, so Herdr’s own tab bar and sidebar agree with it.
* **Desktop**: drag a tab in the sidebar above or below another tab of the same group; a line shows where it lands. With a tab focused, **Alt+↑** and **Alt+↓** move it one place. The tabs in the header row drag too, as in a browser: the tab slides along the tab bar only, never over the page, and the tabs it passes slide aside to make room; **Esc** puts it back. Or press **Alt+←** and **Alt+→** with one focused, or use **Move left** and **Move right** in its menu (right click).
* **Mobile**: the **Current session** row of the sessions sheet has **↑** and **↓** buttons that move the tab on screen within its group.
* **Herdr workspaces**: **Move up** and **Move down** in a workspace’s **⋯** menu, or drag its header on desktop. A repository’s workspace moves together with its worktrees, as in Herdr; a linked worktree cannot be moved on its own.
* **herdr-core spaces**: as Herdr workspaces, once the herdr app has run on this machine. The order is written into the herdr app’s layout, so its sidebar and tab bar follow.
The controls are hidden while a search, the agent filter or “blocked first” sorting is on, since the list on screen is then not the real order; a line in their place (the **Current session** row, a workspace’s **⋯** menu) says what to turn off to get them back. Not supported: tmux and Zellij sessions (neither has a session order), tmux older than 3.2, psmux on Windows, Herdr older than 0.8.0, Herdr on Windows for now, and herdr-core before the herdr app has run on the machine.
Caution
With tmux, a tab’s id is its window number, and moving a window renumbers the windows it passes. Termote keeps your unsent Chat message, attached images and unsaved file edits with their window, but a link to a tab (or another device’s saved selection) that named a renumbered window opens another tab afterwards.
### Searching Sessions
[Section titled “Searching Sessions”](#searching-sessions)
With more than one session, a magnifier in the desktop sidebar’s header (and in the mobile sessions sheet’s header, next to **New session**) opens a search box above the list. Type part of a name and the list keeps only the sessions that match.
* A session matches on its name, its description, its group’s name and branch, the agents it runs, the processes in its panes and their labels. Case and Vietnamese diacritics do not matter, and with several words each must match.
* The search narrows the [agent filter](#filtering-by-agent-status): a session must match both. Groups with no match are hidden and groups with one are shown open, as with the filter.
* **Enter** opens the first session in the list and **↓** moves to it (desktop). **Esc** clears the box, and on an empty box closes it.
* On desktop the search stays after you pick a session, so you can move between the matches. The sessions sheet opens on the whole list each time.
* The search is not saved, and it never takes keys away from the terminal: it reads keys only while its box has focus.
### Filtering by Agent Status
[Section titled “Filtering by Agent Status”](#filtering-by-agent-status)
When at least one session runs a coding agent, a filter bar appears above the session list (in the desktop sidebar and in the mobile sessions sheet):
| Filter | Lists |
| ----------- | ------------------------------------------- |
| `All` | Every session |
| `Needs you` | Sessions with a pane whose agent is blocked |
| `Working` | Sessions with a pane whose agent is working |
| `Agents` | Sessions running any coding agent |
* Each filter shows how many sessions it lists. Counts are per session, not per pane, and a session with one blocked and one working pane counts under both.
* While a filter is on, groups with no matching session are hidden, and groups with one are shown open. Clearing the filter puts every group back the way you left it.
* With no match, the list says so and offers **Show all**.
* The collapsed desktop sidebar lists only the matching sessions too, and a dot on its expand button shows that a filter is on.
* The filter is saved on this device. With no agent in any session, the bar is hidden and every session is listed, whatever filter was saved.
* To keep blocked sessions at the top of each group instead, see [Blocked Sessions First](/usage/settings/#blocked-sessions-first).
## Session Features
[Section titled “Session Features”](#session-features)
| Feature | Description |
| ----------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Custom icons | Choose emoji icon for each session; until you choose one, a session running a coding agent shows that agent’s logo (Claude Code, Codex, Gemini CLI, Copilot, Cursor, OpenCode and others; 🤖 for an agent without one); **Automatic** in the icons goes back to that |
| Rename | Change session name anytime; on herdr-core once the herdr app has run on this machine (until then Edit changes only the icon) |
| Auto-sync | Groups/tabs/panes sync from the server (configurable interval) |
| Persistence | Sessions backed by tmux windows, Herdr tabs or Zellij tabs |
| Connection status | Real-time indicator in header (see below) |
## Deep Links
[Section titled “Deep Links”](#deep-links)
Every screen has an address you can copy and open later or on another device:
```text
<base>/#/s/<group>/<tab>[/<pane>][?view=terminal|chat|files|changes][&solo=1][&file=<path>[&staged=1][&orig=<path>]]
<base>/#/overview[?solo=1]
```
* `group`, `tab` and `pane` are the IDs from `/api/mux/snapshot`. With tmux and Termote’s own session, `group` is the session name and `tab` the window index (`#/s/main/0`); another session uses tmux’s session id, `group` `$3` and `tab` `$3:1` (URI-encoded), which stays the same when the session is renamed but changes when the tmux server restarts. There is no `pane`. With Herdr they are the IDs Herdr assigns to the workspace, tab and pane. With Zellij they come from the session name, so renaming a session outside Termote changes them (renaming it in Termote keeps your selection).
* Opening a link selects that group, tab and pane once the session list has loaded. An unknown ID keeps the current session and shows a toast.
* `view` is used only when that view is available; otherwise the terminal opens.
* `file` opens a file in the **Files** view (`view=files`), or its diff in **Changes** (`view=changes`; `staged=1` for the staged side, `orig` for a rename’s old path). The path is relative to the pane’s directory, URI-encoded, and the server opens it under that directory as it does any other file: a path that leaves it, or a file that is not there, shows “File not found or not allowed” and nothing else.
* `solo=1` opens the link in a focus window (see [Open in a New Window](#open-in-a-new-window)).
* A link only selects. It never sends keys and never creates or closes a session. With tmux, selecting changes the current window, which every client of that tmux session shares.
* Switching session, tab or pane updates the address bar without adding history entries, so copying the address is enough. **Copy link** in the **More** (⋯) menu copies it to the clipboard.
* `termote url` prints a link from the command line, and the [Herdr plugin](/usage/herdr-plugin/) opens one on the Herdr pane you are on.
### Open in a New Window
[Section titled “Open in a New Window”](#open-in-a-new-window)
On a desktop (a mouse and a wide screen), a file, a diff, a tab, a pane or the agent overview can open in a window of its own, next to the one you work in:
* **A file**: **Open in new window** in the file’s header in the Files panel.
* **A diff**: the **Open in new window** button next to **Copy path** in the Changes panel.
* **What is on screen** (the pane, its tab, or the overview): **Open in new window** in the **More** (⋯) menu, or the **Open in new window** command of the command palette.
* **Any tab or pane**: in the command palette’s tab list (`@`), `Ctrl`+`Enter` (`⌘`+`Enter` on a Mac) opens the highlighted one in a new window instead of switching to it.
Installed as an app (Chrome or Edge), it opens a new app window; in a browser, a new tab. `Shift`+click asks for a separate, sized window instead. A blocked pop-up says so. Phones and tablets have no second window, so the action is not offered there.
The new window is a **focus window** (`solo=1` in its address): it shows only that target, without the sessions sidebar, the tab bar or the pane strip. A file or diff fills the window; restoring its panel shows the terminal next to it. It keeps nothing on the device: the main window’s open file tabs, selected tab, pane views and unsaved drafts stay as they were, whatever you open or close in the focus window. Settings such as the theme or the font size are this device’s and are saved as usual. **Open in main window** in its header hands the target back to the window that opened it (or a new one when that window is gone) and closes the focus window. A focus window opens no further windows and does not switch to another tab or pane: those actions are offered in the main window.
Files open editable, as in the main window. When the same file is open in both and one saves first, the other’s save is refused with “changed on the host”, as with any other change made elsewhere.
Caution
With tmux, a focus window on a tab that is not the current window of its session switches that session’s window, for every client attached to it, including the main window and other devices (see [the link rules](#deep-links) above). Herdr and Zellij pick the pane per window and are not affected.
Each window that shows a terminal holds one terminal stream; a focus window on a file holds none while the file covers the terminal, and Herdr’s side-by-side panes hold one each. The server keeps at most 16 streams with Herdr and 8 with tmux and Zellij, and closes the oldest for a new one. Only that window’s stream closes, never the session or what runs in it, and the window shows **Disconnected by a newer window — Reconnect** instead of reconnecting on its own (two windows would otherwise push each other out in turn). Click it, or the connection indicator, to reconnect.
## Herdr Backend
[Section titled “Herdr Backend”](#herdr-backend)
Native installs ([`termote start --mux herdr`](/installation/native/#herdr-backend)) and the container ([`termote container up --mux herdr`](/installation/docker/#terminal-backend)) can use Herdr instead of tmux. It changes the session model in two ways:
* **Panes**: a Herdr tab can have more than one pane. When it does, a **pane strip** appears below the tab bar to switch between them. On a computer, a tab of 2 to 4 panes also shows its panes side by side, in the Herdr desktop’s layout, all at one font size that fits the window: click a pane (or pick it in the pane strip) to type into it; Herdr’s own focus does not move. A zoom, a split or a resize made on the desktop shows within a second. A pane closed by a newer window shows **Reconnect** in its own box. Termote shows one pane at a time instead on a phone, on a view-only device, with more than 4 panes, on Herdr older than 0.9.3 or on Windows, when the panes would be too small to read, or with **Show herdr panes side by side** off (Settings → Terminal). A tab past 4 panes says so in a notice: when it goes past, or when it opens past them, once per tab until the page reloads. Each pane shows the command it runs under its name. To close a pane, click the X next to its name (shown on the streamed pane, and on the others on hover or on touch screens), then confirm with “Close pane”. The other panes of the tab stay open; a tab’s last pane is closed by closing the tab.
* **Agent status badges**: each pane can show a small icon — idle, working, blocked, or done — reflecting the coding agent Herdr detected in that pane. Badges refresh at most once per the configured **Session poll interval** (Settings → Sessions), not instantly.
Caution
Selecting a tab or pane in the PWA never changes what is shown on the Herdr desktop — it only changes which pane this client streams. There is no “show on desktop” action. In the container, Herdr runs inside it and sees only the container’s terminals.
## Zellij Backend
[Section titled “Zellij Backend”](#zellij-backend)
Native installs on Linux and macOS ([`termote start --mux zellij`](/installation/native/#zellij-backend)) and the container ([`termote container up --mux zellij`](/installation/docker/#terminal-backend)) can use Zellij 0.45.0 or later instead of tmux. A group is a Zellij session, a tab a Zellij tab and a pane a terminal pane (plugin panes are not shown). Every running session of yours is listed, as with tmux; sessions kept only for resurrection (EXITED) are not. When no session runs, Termote starts `main`.
* **Tabs**: each device selects its own tab, as with Herdr; your own Zellij client never moves. A tab with more than one pane shows the **pane strip**. Tabs can be renamed, closed and reordered; sessions cannot be reordered.
* **New tab**: Zellij lays a tab out only when a client is attached to the session, so adding a tab to a session nobody has open (no terminal and no Termote on it) says “open this Zellij session in a terminal (or in Termote) before adding a tab”.
* **Terminal**: Termote’s terminal is a real Zellij client, so Zellij’s tab bar and status bar show, and a tab takes the size of the smallest client viewing it. It is in Zellij’s locked mode, so the toolbar’s Ctrl keys reach the pane; your own locked-mode bindings still act (the default config’s Ctrl g unlocks). A click on another pane or tab inside the terminal closes the stream, and Termote reopens it on its pane.
* **Scrolling**: there is no server-side scrolling. The mouse wheel scrolls through Zellij itself, for every client viewing that pane; Select text still shows the pane’s history.
* **View-only devices** see the pane’s screen, without history scrolling or input.
* **Chat view** (Claude Code, Codex) and **Files/Changes** work on Linux and macOS. On macOS a session renamed after it started has no Chat view until it is restarted.
Caution
After upgrading Zellij, Termote refuses to create sessions while sessions of the old version still run; restart those sessions (or Termote).
## Connection Indicator
[Section titled “Connection Indicator”](#connection-indicator)
The header displays a real-time connection status indicator showing your link to the server:
| Status | Indicator | Meaning |
| ------------------------------ | --------- | ----------------------------------------------------------------- |
| Connected | Green dot | Server online, terminal responsive |
| Connecting | Blue dot | Establishing connection to server |
| Disconnected | Red dot | No connection to server (clickable to retry) |
| Error | Red X | Connection error occurred |
| Disconnected by a newer window | Red dot | A newer window took this window’s stream (clickable to reconnect) |
The indicator auto-detects disconnects on both the snapshot polling and the terminal WebSocket. Click it when disconnected to manually retry.
Tip
Sessions survive network disconnects. Reconnect to pick up where you left off.
## tmux Commands
[Section titled “tmux Commands”](#tmux-commands)
```bash
# List windows (sessions)
tmux list-windows
# Create new window
tmux new-window -n myproject
# Switch to window
tmux select-window -t 0
# Kill window
tmux kill-window -t 0
```
### Worktrees
[Section titled “Worktrees”](#worktrees)
A group in a git repository can open git worktrees as groups of their own, from its **⋯** menu. It works with Herdr 0.9.2 or later, tmux, Zellij and herdr-core, and on Windows with psmux too.
* **New worktree** asks for a branch, a base and an optional name. A new branch is made from the base (**Current HEAD** by default, or one of the repository’s local branches); a branch that exists is checked out as it is, so the base is hidden. The new group opens once the checkout is made. The folder is picked for you:
* Herdr: Herdr picks it (`~/.herdr/worktrees/<repo>/<slug>`, on Windows `%USERPROFILE%\.herdr\worktrees\<repo>\<slug>`; the slug is the branch in lowercase letters, digits and `-`).
* herdr-core: `~/.herdr/worktrees/<repo>/<slug>`, where the herdr app keeps its own, so it lists them too.
* tmux, psmux and Zellij: `~/.termote/worktrees/<repo>/<slug>` (on Windows `%USERPROFILE%\.termote\worktrees\<repo>\<slug>`, a folder only your user can open); in the container `/workspace/.termote/worktrees/<repo>/<slug>`, so they survive `container down`. git records that container path in the checkout’s `.git` file and in the repository’s own worktree list, so git on the host sees such a worktree as prunable: run `git worktree prune` there and it forgets it (the folder stays; `git worktree repair <path>` from the host links it again). With `--workspace` set to the repository itself, `.termote/worktrees` is a folder inside that checkout: add `.termote/` to its `.gitignore` or `.git/info/exclude`.
On tmux, Zellij and herdr-core the slug is the branch with every run of characters other than letters, digits, `.`, `_` and `-` turned into one `-`. Either way `feat/x` and `feat-x` share one folder, and the second is refused. On tmux and Zellij the new group is named after the slug unless you give a name; if a group of that name exists, the checkout is kept and **Open worktree** opens it.
* **Open worktree** lists the repository’s other worktrees: one already open is shown, a closed one is opened.
* **Remove worktree**, on a worktree’s own group, names its branch and folder and asks first. It ends the group’s terminals, closes the group and deletes the folder, files git ignores (`.env`, `node_modules`) included; the branch is kept. A worktree with uncommitted or untracked changes is asked about a second time: removing it then loses those changes. On tmux, Zellij and herdr-core the group closes first; if git still refuses then, the checkout stays and the group stays closed. On Windows (psmux, herdr-core) a folder cannot be deleted while a program still has a file in it open: termote waits up to 10 seconds for the closed group’s programs to end, and if one still holds a file (an editor, a dev server started elsewhere), it says so and deletes nothing. Close that program and remove again.
A worktree’s group shows its branch next to its name, indented under its repository’s group as in Herdr; collapsing the repository’s group hides its worktrees too, and its status badge then shows theirs as well. On tmux, Zellij and herdr-core a group counts as a worktree when its first pane (first tab) works in one; a group just opened or moved with `cd` can take a few seconds to show under its repository.
With Herdr, creating and removing run git with your own config, hooks included, as typing the command would. On tmux, Zellij and herdr-core termote runs git itself with hooks off (no `post-checkout`) and filters off for the checkout: a Git LFS repository gets pointer files, so run `git lfs pull` in the new worktree’s terminal. Branch names with spaces, control or invisible characters are refused.
## API Endpoints
[Section titled “API Endpoints”](#api-endpoints)
Sessions are managed via termote’s `/api/mux/*` endpoints:
| Endpoint | Method | Description |
| ----------------------------- | ------ | -------------------------------------------------------------------------------------- |
| `/api/mux/snapshot` | GET | List all groups/tabs/panes |
| `/api/mux/tabs` | POST | Create tab (body: `{groupId, name}`) |
| `/api/mux/tabs/{id}/select` | POST | Switch tab (tmux; Herdr and Zellij select client-side and answer 501) |
| `/api/mux/tabs/{id}` | PATCH | Rename tab (body: `{name, key}`; the tab’s `key` refuses a stale id with 409) |
| `/api/mux/tabs/{id}?key=` | DELETE | Remove tab (same `key` check) |
| `/api/mux/tabs/{id}/move` | POST | Move a tab within its group (body: `{index}`; answers its new `id`) |
| `/api/mux/groups/{id}/move` | POST | Move a Herdr workspace (body: `{index}`) |
| `/api/mux/panes/{id}` | DELETE | Close a pane (psmux answers 501) |
| `/api/mux/panes/{id}/split` | POST | Split a pane (body: `{direction: right\|down, cwd?}`; answers the new pane’s `id`) |
| `/api/mux/panes/{id}/zoom` | POST | Zoom a pane (body: `{mode: toggle\|on\|off}`) |
| `/api/mux/panes/{id}/swap` | POST | Swap with a neighbour (body: `{direction: prev\|next}`; 409 `no_neighbor` past an end) |
| `/api/mux/panes/{id}/resize` | POST | Resize (body: `{direction: left\|right\|up\|down, amount}`, 1–50 cells) |
| `/api/mux/groups` | POST | Create a group (body: `{name, cwd}`) |
| `/api/mux/groups/{id}` | PATCH | Rename a group (body: `{name}`) |
| `/api/mux/groups/{id}` | DELETE | Close a group and everything in it |
| `/api/mux/worktrees?groupId=` | GET | A group’s worktrees and local branches (`caps.worktrees`) |
| `/api/mux/worktrees` | POST | Create a worktree (body: `{groupId, branch, base, label}`) |
| `/api/mux/worktrees/open` | POST | Open a worktree (body: `{groupId, branch}`) |
| `/api/mux/worktrees/{id}` | DELETE | Remove a worktree group (body: `{force, path, branch}`) |
## Troubleshooting
[Section titled “Troubleshooting”](#troubleshooting)
### Session not persisting
[Section titled “Session not persisting”](#session-not-persisting)
1. Check tmux is running: `tmux list-windows`
2. Check termote logs: `termote container logs` (container) or `termote logs server` (native)
### WebSocket / terminal errors
[Section titled “WebSocket / terminal errors”](#websocket--terminal-errors)
1. Check termote logs: `termote container logs` or `termote logs server`
2. The terminal WebSocket is `/api/mux/stream`, served by termote itself
3. Check server health: `termote status` (container: `termote container status`)
# Settings
> Every Termote setting: interface style, toolbar, terminal font, agent notifications, sidebar, editor, updates, and where the settings are saved.
Open settings from the **More** (⋯) menu in the header → **Settings**. Settings are grouped: **Appearance**, **Keyboard**, **Terminal**, **Sessions**, **Files** and **Data & help**. On desktop it is a two-column dialog with a group list on the left. On a phone, or in a window narrower than 768 px, it is a full-screen sheet that opens at the list of groups; Appearance and Keyboard show their current values under their names. Tap a group to open it, and **‹** or the phone’s Back to return to the list; the close button or Esc closes Settings from anywhere. Help & gestures and About open in the same kind of sheet from the **More** menu.
## Interface Style
[Section titled “Interface Style”](#interface-style)
Settings → Appearance → **Interface style** changes how the whole app looks. The change applies at once and is saved in this browser.
| Style | Look | Default |
| -------- | --------------------------------------------------- | ------- |
| Neutral | Balanced defaults, indigo accent | ✓ |
| Terminal | Monospace labels, small corner radius, green accent | |
| Native | Large corner radius, system-app feel, blue accent | |
The style is independent of the **theme** (Light, Dark, System), which you pick right below it in Settings → Appearance → **Theme**, or in the **More** (⋯) menu. Any style works with any theme. If you have turned on reduced motion in your operating system, Termote skips its animations.
## Text Input Send Behavior
[Section titled “Text Input Send Behavior”](#text-input-send-behavior)
Controls what happens when you tap **Send** in the text input mode (non-Latin input).
| Option | Behavior | Default |
| -------------- | --------------------------------------------- | ------- |
| Send text only | Sends text to terminal, no Enter key appended | ✓ |
| Send + Enter | Sends text then automatically presses Enter | |
**Use case**: Set to “Send + Enter” if you want to type a command in Vietnamese/CJK and submit it in one tap.
## Paste Button Source
[Section titled “Paste Button Source”](#paste-button-source)
Controls where the **Paste** button in the toolbar gets content from.
| Option | Behavior | Default |
| ---------------- | -------------------------------------------- | ------- |
| System clipboard | Paste from device clipboard (Ctrl+Shift+V) | ✓ |
| tmux buffer | Paste from tmux copy mode buffer (Ctrl+B, ]) | |
**Tip**: Use “System clipboard” for pasting from other apps. Use “tmux buffer” for pasting text copied within tmux.
With the tmux backend the second option is labelled **tmux buffer**; with any other backend it is labelled **Session buffer**. With the Herdr, Zellij and herdr-core backends there is no tmux buffer, so this setting is hidden and the Paste button always uses the system clipboard.
## Toolbar Default Expanded
[Section titled “Toolbar Default Expanded”](#toolbar-default-expanded)
Controls whether the keyboard toolbar starts in expanded or minimal mode when the page loads.
| Option | Behavior | Default |
| ------ | ------------------------------------------- | ------- |
| Off | Toolbar starts in minimal mode (fewer keys) | ✓ |
| On | Toolbar starts expanded (all keys visible) | |
## Disable Right-Click Menu
[Section titled “Disable Right-Click Menu”](#disable-right-click-menu)
Blocks the browser context menu (right-click / long-press) on the terminal area to prevent accidental interruptions.
| Option | Behavior | Default |
| ------ | --------------------------------------- | ------- |
| On | Context menu is blocked on terminal | ✓ |
| Off | Browser default context menu is allowed | |
## Copy on Select
[Section titled “Copy on Select”](#copy-on-select)
Copies the text you select in the terminal with the mouse as soon as you release the button, and shows a short “Copied” toast. Off, select the text and press Ctrl+Shift+C (Cmd+C on a Mac) instead; see [Copy text](/usage/keyboard/#copy-text).
A program that takes the mouse (tmux with `set -g mouse on`, vim with `mouse=a`, htop) receives a plain drag itself, so nothing is selected. Hold Shift while dragging (Option on a Mac) to select and copy the text anyway; the first such drag on a page shows a toast that says so.
| Option | Behavior | Default |
| ------ | ------------------------------------------------ | ------- |
| Off | A selection is copied with Ctrl+Shift+C or Cmd+C | ✓ |
| On | A selection is copied when the mouse is released | |
## Draw the Terminal with the GPU
[Section titled “Draw the Terminal with the GPU”](#draw-the-terminal-with-the-gpu)
Draws the terminal with WebGL2 on this device’s GPU, which keeps large output smooth. Where the browser has no WebGL2, or its WebGL2 runs on the processor instead of a GPU (a virtual machine, a browser with GPU acceleration off), or loses it, the terminal draws as before, without the GPU; after the app has been in the background, it tries the GPU again. On a computer, up to 8 terminals of a page use the GPU, the others draw without it. Turn it off if text looks blurry, flickers or has the wrong colours on this device. The setting stays with this device: Export and Import do not carry it.
| Option | Behavior | Default |
| ------ | ----------------------------------------------- | ------- |
| On | The terminal is drawn with WebGL2 where it runs | ✓ |
| Off | The terminal is drawn without the GPU | |
## Show Herdr Panes Side by Side (Herdr, Desktop Only)
[Section titled “Show Herdr Panes Side by Side (Herdr, Desktop Only)”](#show-herdr-panes-side-by-side-herdr-desktop-only)
On a computer, a Herdr tab of 2 to 4 panes shows its panes side by side, in the Herdr desktop’s layout, each one its own terminal, all at the font size that fits the window. Click a pane, or pick it in the pane strip, to type into it. With the switch off, or when the panes would be too small to read, Termote shows one pane at a time with the pane strip. See [Herdr Backend](/usage/sessions/#herdr-backend).
| Option | Behavior | Default |
| ------ | ---------------------------------------- | ------- |
| On | A split tab shows its panes side by side | ✓ |
| Off | One pane at a time | |
This setting only appears with Herdr 0.9.3 or later, not on Windows, or with herdr-core (`--mux hc`, the herdr app’s layout), on a computer with full rights.
## Fit Herdr Pane to This Device (Herdr Only)
[Section titled “Fit Herdr Pane to This Device (Herdr Only)”](#fit-herdr-pane-to-this-device-herdr-only)
A Herdr pane has one size, shared by the desktop and every device that shows it. By default the PWA shows the pane at the desktop’s size: the font buttons then zoom in and out, and a pane larger than the screen can be dragged.
With this switch on, the PWA sets the pane’s size to fit its own screen while the page shows the terminal, as it does with tmux: the font buttons change how many columns and rows the pane has. The pane on the desktop takes the same size until the page is hidden or closed, another view such as Chat opens, the connection is lost (after up to about 30 seconds), or another device with the switch on takes over. Under the Chat view the pane keeps the desktop’s size, so a Claude Code dialog taller than a phone’s screen is still shown whole and can be answered there. A device that lost the size shows a toast and keeps showing the pane at the desktop’s size; it takes the size back the next time the page is shown.
| Option | Behavior | Default |
| ------ | --------------------------------------- | ------- |
| Off | Pane keeps the desktop’s size | ✓ |
| On | Pane fits this device while it is shown | |
This setting only appears with the Herdr and herdr-core backends. On macOS, if the server is killed (`kill -9`) while a device drives the size, the pane keeps that size until the service starts again.
## Sidebar Layout (Desktop Only)
[Section titled “Sidebar Layout (Desktop Only)”](#sidebar-layout-desktop-only)
How the desktop sidebar lists your sessions. Settings → Sessions → **Sidebar layout**.
| Option | Behavior | Default |
| -------- | ----------------------------------------------------------------------------- | ------- |
| Detailed | Each workspace with its tabs under it, the agent filter above the list | ✓ |
| Compact | One line per workspace, its tabs in the header, the running agents underneath | |
The compact layout always shows the session tabs in the header, since that is where a workspace’s tabs are: **Show session tabs** is then on and locked. The agent filter is hidden while it is compact; the filter you saved comes back with the detailed layout. A phone keeps its sessions sheet whichever you pick. See [Compact Layout](/usage/sessions/#compact-layout).
## Show Session Tabs (Desktop Only)
[Section titled “Show Session Tabs (Desktop Only)”](#show-session-tabs-desktop-only)
Displays browser-like session tabs in the header row for quick session switching on desktop. The sidebar remains available for more detailed session management.
| Option | Behavior | Default |
| ------ | --------------------------- | ------- |
| On | Show session tabs in header | ✓ |
| Off | Hide tabs, use sidebar only | |
This setting only appears on desktop devices. With the compact [sidebar layout](#sidebar-layout-desktop-only) the tabs are always shown and the switch is locked.
When the tabs no longer fit, the view and panel buttons beside them drop their labels and show their icons only (hover one for its name); the labels come back once the tabs have room again.
## Toolbar Position (Desktop Only)
[Section titled “Toolbar Position (Desktop Only)”](#toolbar-position-desktop-only)
Where the buttons that change what the pane shows go. Settings → Sessions → **Toolbar position**.
| Option | Behavior | Default |
| --------- | -------------------------------------------------------------------------------------------- | ------- |
| Header | View and panel buttons and the Pane menu in the header; palette and fullscreen in the ⋯ menu | ✓ |
| Right bar | Those buttons, fullscreen and the palette on a narrow bar down the right edge | |
With the right bar the header keeps the session tabs, the usage chips, the font size, the connection and the ⋯ menu, so more tabs fit. The bar stays in reach when the Files or Changes panel is maximized, and the terminal narrows to fit beside it. Up and Down switch between the views on the bar. A phone keeps its own header whichever you pick.
## Blocked Sessions First
[Section titled “Blocked Sessions First”](#blocked-sessions-first)
Lists sessions with an agent waiting on you at the top of each group in the sidebar. The other sessions keep their order. It is off by default, because sessions then move whenever an agent changes state.
| Option | Behavior | Default |
| ------ | ------------------------------------ | ------- |
| Off | Sessions keep their order | ✓ |
| On | Blocked sessions first in each group | |
The compact [sidebar layout](#sidebar-layout-desktop-only) lists no sessions to sort, so the switch is locked there and the setting has no effect; the value you saved comes back with the detailed layout.
## Estimated Cost
[Section titled “Estimated Cost”](#estimated-cost)
Shows what an agent’s tokens would cost at API list prices beside the token counts: on the [overview](/usage/overview/#tokens-and-cost)’s cards and strip, and in the session history. A subscription is not billed by the token, so the cost is only an estimate. For OpenCode it is the cost OpenCode recorded at your provider’s prices. Off, every place shows the tokens alone.
| Option | Behavior | Default |
| ------ | ------------------------- | ------- |
| On | Tokens and estimated cost | ✓ |
| Off | Tokens only | |
## Notify When an Agent Needs Me
[Section titled “Notify When an Agent Needs Me”](#notify-when-an-agent-needs-me)
Settings → Sessions → **Notify when an agent needs me** shows a notification when an agent’s dialog waits for an answer, or when it finishes a turn. Tapping the notification opens that pane. Without Web Push, the pane you are looking at never notifies. With it, every push shows a notification, that pane included: browsers require it. The notification names the agent, the group and the tab, and never holds screen or transcript text.
Turning it on asks the browser for permission to show notifications. If the browser blocks them, the row reads **Blocked in browser settings**. Allow notifications for the site there, then turn it on again. If the row reads **Reload to turn on**, the page is still running an older version of the app: reload it first.
How the notification arrives depends on the device:
* **With the app closed** (Android Chrome, desktop Chrome, Edge and Firefox): the server sends a Web Push, so it arrives even when no Termote page is open.
* **iPhone and iPad**: it works only in the app added to the Home Screen, on iOS 16.4 or later. In Safari the row reads “iPhone/iPad: works only from the Home Screen app (iOS 16.4+)”.
* **Without Web Push** (an older server, or a browser without it): only an open page notifies, which in practice means a desktop tab, since a phone suspends a page it does not show.
Limits:
* With tmux, only the active pane of each window is watched, so an agent in a split pane that is not active does not notify.
* The server checks every 5 seconds, so a turn shorter than that may not notify.
* `termote start --fresh`, or any change of the password, stops push to every device. A device gets it again once it opens Termote and signs in with the new password.
* Logging out also stops push to that device.
* A browser signed in with the password gets push only while that sign-in lasts: it stops when the sign-in is revoked (Settings, or `termote signins revoke`) or expires (24 hours), and when the server restarts or updates. Opening Termote and signing in again turns it back on. A paired device keeps getting push until it is revoked or expires.
| Option | Behavior | Default |
| ------ | ------------------------------------------- | ------- |
| Off | No notifications | ✓ |
| On | Notify when an agent is blocked or finished | |
## Count on the App Icon
[Section titled “Count on the App Icon”](#count-on-the-app-icon)
Settings → Sessions → **Count on the app icon** shows a number on the icon of the installed app: how many agents wait on you. An agent counts when its dialog waits for an answer, or when it finished a turn that no one has seen yet. The count goes down when you answer the agent, or when you open its pane on any device signed in with full rights, or when you read it in a tmux or Zellij terminal on the desktop. A device with view-only rights shows the count but never lowers it.
The server remembers which turns were seen, for every device at once. On tmux, a pane counts as seen while its window is the one shown. A desktop terminal counts for every pane of the window (tmux) or tab (Zellij) it shows. One attached to tmux counts only while you typed in it in the last 5 minutes; one attached to Zellij always counts. With Herdr, a turn seen in Termote shows as finished but read, even if the Herdr app still marks it.
The row shows only where the browser supports it: the installed app on Windows, macOS and ChromeOS, and the Home Screen app on iOS 16.4 or later, which also needs notifications allowed. Android shows only a dot for the notifications, and Chromium on Linux shows nothing.
Limits:
* With the app closed, the count changes only when a push arrives (**Notify when an agent needs me** on). Answering on another device does not lower it until the next push, or until the app opens.
* A server restart forgets which turns were seen, and a turn shorter than the poll may not count.
* psmux on Windows never counts a desktop terminal as a viewer.
* Logging out clears the count.
| Option | Behavior | Default |
| ------ | ------------------------------------------------------- | ------- |
| Off | No count on the icon | |
| On | Count the agents blocked or with a turn no one has seen | ✓ |
## Terminal Font
[Section titled “Terminal Font”](#terminal-font)
The font the terminal uses, typed by name (e.g. `JetBrainsMono Nerd Font`). It must be installed on the device the browser runs on; if it is not, the terminal falls back to the system monospace font. Several names can be given, separated by commas. The value is saved when the field loses focus or you press Enter.
Nerd Font icons (prompt themes such as Starship or Powerlevel10k, file icons in `eza` or `lsd`) display without setting anything: Termote ships the Symbols Nerd Font and uses it for any icon the chosen font lacks, including on phones where no Nerd Font can be installed.
| Option | Behavior | Default |
| ----------- | ------------------------------------------------------- | ------- |
| Empty | System monospace font, icons from the bundled font | ✓ |
| A font name | That font first, then the system font and bundled icons | |
## Session Poll Interval
[Section titled “Session Poll Interval”](#session-poll-interval)
Controls how often the app syncs the session list from the server. Lower values keep multi-device views in sync faster; higher values reduce server load when using a single device.
| Option | Default |
| --------------------------------- | ------- |
| 3s, 5s, 10s, 15s, 30s, 1m, 2m, 5m | 5s ✓ |
## Excluded Folders
[Section titled “Excluded Folders”](#excluded-folders)
Settings → Files → **Excluded folders** is the list of folder names that [Find a file](/usage/files/#finding-a-file) never enters. They apply only to files git ignores and to folders outside a repository; a file git tracks is always searched, even under one of these names.
* Remove a name with the **x** on it. Add one by typing it in **Folder name** and pressing **Add** (or Enter). A name is a single folder name, such as `node_modules`: no `/`, no `*`, `?` or `[`, not `.` or `..`, and at most 255 bytes. The field says why a name is refused. At most 50 names.
* **Reset to defaults** restores the list below.
| Default folders |
| ------------------------------------------------------------------------------------------------------------------------------------- |
| `node_modules`, `.venv`, `venv`, `__pycache__`, `dist`, `build`, `target`, `.next`, `.nuxt`, `.turbo`, `.cache`, `coverage`, `vendor` |
The **Include ignored** switch is not here: it sits next to the Find a file box in Files.
## Editor
[Section titled “Editor”](#editor)
Settings → Files also sets how a file is edited on a computer with a mouse (see [Editing a File](/usage/file-editing/#editing-a-file)). A phone or a touch tablet edits in a plain text box: only **Wrap lines** applies there, and on a phone lines always wrap at first.
| Setting | What it does | Default |
| -------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------ | ------- |
| **Wrap lines** | Long lines of a file wrap when it opens in Files or Changes, on a computer too. The wrap button in the file’s bar still turns it on or off there | Off |
| **Editor font size** | Size of the editor’s text, 11 to 20 px | 13 px |
| **Tab size** | Columns of a tab, and of one indent: 2, 4 or 8. A file indented mostly with tabs is indented with a tab | 4 |
| **Vim mode** | Vim keys in the editor; `:w` saves. Loaded the first time it is turned on | Off |
## Prompt Templates
[Section titled “Prompt Templates”](#prompt-templates)
Settings → Prompt templates lists the templates this server keeps for every device: make, edit and delete them, and export or import them as JSON. A view-only device only lists and exports them. See [Prompt Templates](/usage/prompt-templates/).
## Tasks
[Section titled “Tasks”](#tasks)
Settings → Tasks keeps the commands you run from a pane’s Tasks view: make, edit and delete them. It also sets **History read for Send to agent**, how many lines of a task’s tab are searched for its run’s output (400 by default); only the run’s own output is sent. A view-only device has no Tasks group. See [Tasks](/usage/tasks/).
## Updates
[Section titled “Updates”](#updates)
Shows the version the server runs and the newest stable release on GitHub (the one `termote update` installs). The browser asks `api.github.com` itself when the app opens or is shown again, at most once an hour (ten minutes after a failed ask); **Check for updates** asks again. When the server is older, this group shows the command for this install with a **Copy** button and a link to the release notes, About names the version on its **Check for updates** row, and a dot marks the header’s **⋯** menu, whose **Update to vX.Y.Z** item opens this group (not for a view-only device or a git checkout):
| Install | Command |
| -------------------------- | ------------------------------------------------------ |
| `install.sh`/`install.ps1` | `termote update` |
| Container | `termote update && termote container up` (on the host) |
A git checkout gets a hint instead of a command (pull, rebuild and restart it; the commands differ by OS).
### Reloading after an update
[Section titled “Reloading after an update”](#reloading-after-an-update)
The page comes from the server, so once the server runs a new version a banner reads **Termote vX.Y.Z is ready** with a **Reload** button (also offered in **Updates**). Termote never reloads on its own for a new release, since an unsaved edit in Files would be lost. It notices the new version when the app opens, when the terminal reconnects after the server restarted, and when the app is shown again. There is no need for **Clear cache & reload**. Only a server whose API changed in a way this page cannot use reloads it at once. With several tabs open, **Reload** reloads only its own tab; the others show the banner.
Updating from 1.13 or older
Pages of those versions do not know this banner yet. After the first update past them, close every Termote tab (on a phone, quit the installed app) once; the new version loads when it opens again.
Once this browser runs a newer stable version than the last one it ran, **Updates** asks once whether you would star Termote on GitHub, with a **Star on GitHub** link and a **No thanks** button. Either one ends it for good; nothing in Termote depends on a star, and the browser keeps only the version it last ran and whether it asked.
## Clear Command History
[Section titled “Clear Command History”](#clear-command-history)
Deletes all saved commands from the command history dropdown. Use when you want to free up stored history or start fresh.
## Welcome Screen and Tour
[Section titled “Welcome Screen and Tour”](#welcome-screen-and-tour)
The first time a device opens Termote, a **welcome screen** shows what this install is: the backend, how many sessions and panes run, which agents wait for you, whether this device can type or only watch, and whether notifications are on. It suggests at most two things to do next (open the pane that needs you, start a session, turn on notifications, add the app to the home screen, pair a phone) and lists the main parts of the app. On a phone it is a sheet; on desktop a dialog. It shows once per device, and not when the page opens on a link to a pane (from a notification).
**Take the 1-minute tour** on it, or **Take the tour** in the **Data & help** group, dims the app and lights its parts one at a time: on a phone the session chip, the view menu, the keyboard toolbar and the gestures; on desktop the sidebar, the views and side panel, the windows and panes, and the command palette. A part not on screen (a collapsed sidebar, a single pane) is left out. Next, Back and Skip move through it; so do → or Enter, ← and Esc.
**Show welcome screen** in **Data & help** opens it again.
### Language
[Section titled “Language”](#language)
Settings → Appearance → **Language** picks the language of the whole app, English or Tiếng Việt; **Auto** follows the browser’s (Vietnamese when the browser’s first language is, English otherwise). It applies at once, without a reload: every screen, dates and times, and the words of push notifications. The **EN | VI** switch on the welcome screen sets the same thing.
A device that picked Tiếng Việt before the whole app was translated (on the welcome screen, or in Settings when the setting covered only the welcome screen and the tour) switches to Vietnamese with the update. A banner says so, with **Switch to English**, until either of its buttons is pressed.
## Export and Import
[Section titled “Export and Import”](#export-and-import)
Settings → Export & import moves this browser’s settings to another browser: **Save to a file** or **Copy**, then in the other browser **Import a file…**, or paste the text and press **Review**. A table lists each setting as it is now and as it would be, and nothing changes until **Apply**. Notifications and the count on the app icon stay with each device, since turning them on asks for a permission, and so does Draw the terminal with the GPU, which depends on the device’s GPU.
## Settings an Agent Proposes
[Section titled “Settings an Agent Proposes”](#settings-an-agent-proposes)
A coding agent on the server’s machine can propose settings with `termote settings propose` (see [Install with an agent](/installation/agent/#settings-the-agent-proposes)). Every browser open on Termote shows a bar under the top bar: **Review** opens the same before/after table, **Apply** changes that browser only, and **Dismiss** hides the proposal there (its other tabs too). A proposal waits 15 minutes, and a device with notifications on is notified once. Native installs only: the container has no local socket for the CLI.
## Persistence
[Section titled “Persistence”](#persistence)
All settings are saved to your browser’s local storage (`termote-settings` key) and persist across sessions. Clearing browser data or using “Clear cache & reload” (in the **More** menu) will reset settings to defaults.
# Tasks
> Run package.json scripts, Makefile targets and Gradle, Cargo, Go or Python tasks in a tab, and send a failed run's output to an agent.
The **Tasks** view of a pane lists what its folder offers to run: the scripts of its `package.json`, the targets of its Makefile, the tasks of other build tools and task runners (Gradle, Maven, PDM, Hatch, Poe the Poet, Cargo, Go, just, Task, Composer, Deno) and the tasks you saved yourself. **Run** starts one in a tab of its own, in the pane’s folder. When a run fails, **Send to agent** passes its output to a coding agent in one tap, so you do not copy it out of the terminal.
## What Is Listed
[Section titled “What Is Listed”](#what-is-listed)
The folder is the one Files and Changes show: the git repository the pane is in, else the pane’s folder. Nothing runs to list the tasks: Termote reads the files.
* **Scripts**, from `package.json`, in the file’s order, each with the command it runs. When the file also has `pre<name>` or `post<name>` scripts, the row names them: `npm`, `pnpm` and `bun` run them around the script. The package manager is the one `packageManager` names, else the one whose lock file is there (`pnpm-lock.yaml`, `yarn.lock`, `bun.lock`), else npm.
* **Make targets**, from the Makefile make would read (`GNUmakefile`, `makefile`, then `Makefile`). Only targets written out are listed: not variables, special targets such as `.PHONY`, pattern rules or targets built from variables. `make` itself is never run to find them.
* **Other tools**, one section each, from the files in the table below.
* **My tasks**: the tasks saved in [Settings](#your-own-tasks), in every pane.
Each section folds from its header, and stays folded on this device. Typing in the filter opens every section with a match.
### Where Tasks Come From
[Section titled “Where Tasks Come From”](#where-tasks-come-from)
| Section | File read in the folder | Tasks listed | Runs as |
| ------------ | ------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------- |
| Gradle | `build.gradle.kts`, else `build.gradle`, else `settings.gradle(.kts)` | `build`, `test`, `check`, `clean`, `assemble`; `run` with the `application` plugin; `bootRun` with the Spring Boot plugin | `./gradlew <task>` when the folder has `gradlew`, else `gradle` |
| Maven | `pom.xml` | `clean`, `compile`, `test`, `package`, `verify`, `install` | `./mvnw <phase>` when the folder has `mvnw`, else `mvn` |
| PDM | `pyproject.toml`, `[tool.pdm.scripts]` | each script, with its `pre_` and `post_` scripts named | `pdm run <name>` |
| Hatch | `hatch.toml` when there is one, else `pyproject.toml` `[tool.hatch.envs]` | each environment’s scripts: `name` in `default`, else `env:name` | `hatch run <env>:<name>` |
| Poe the Poet | `pyproject.toml`, `[tool.poe.tasks]` | each task but those starting with `_` | `poetry run poe` with `poetry.lock`, `uv run poe` with `uv.lock`, else `poe` |
| Cargo | `Cargo.toml`, `.cargo/config` else `.cargo/config.toml` | `build`, `check`, `test`, `clippy`, `fmt`; `run` with `src/main.rs` or a `[[bin]]`; each `[alias]` | `cargo <name>` |
| Go | `go.mod` | `build`, `test`, `vet` of `./...`; `run .` with a `main.go` | `go <command>` |
| just | `justfile` or `.justfile`, in any case | each recipe that needs no argument | `just <recipe>` |
| Task | `Taskfile.yml` (and the other names go-task reads, in its order) | each task but `internal: true` ones | `task <name>` |
| Composer | `composer.json`, `scripts` | each script | `composer run-script <name>` |
| Deno | `deno.json`, else `deno.jsonc`, `tasks` | each task | `deno task <name>` |
Gradle and Maven are never run to find their tasks, so the list is their standard one, read from the folder’s own build file only (not from sub-projects). A just recipe that is private (`_name` or `[private]`) or takes an argument without a default is left out, and so is a Cargo alias named like a Cargo command (Cargo would run its own) and a Hatch script whose name holds `:`. On Windows, from PowerShell, Gradle and Maven run `.\gradlew.bat` and `.\mvnw.cmd`.
A script or target name with a space or another character a shell would read (`;`, `$`, a quote) is left out, and the list says how many were. A file larger than 1 MB (256 KB for a TOML file) is not read, and a file that cannot be read as its tool’s (invalid JSON or TOML, or both a `justfile` and a `.justfile`) is named at the top of the list. Only the folder’s own files count: the `package.json` of a package inside a monorepo is not listed.
## Running a Task
[Section titled “Running a Task”](#running-a-task)
**Run** opens a tab named `task:<name>` (`task:<tool>:<name>` for another tool’s task, such as `task:cargo:test`) in the pane’s folder and types the command into its shell, as if you had: your `PATH`, nvm and other shell settings apply. The toast’s **Open** shows the tab.
Run the task again and its tab is used again when its shell is waiting at its prompt; a line you left half typed there is cleared first. If the last run is still running, Termote says so and offers **Run in new tab**. When two devices press Run at the same time, one tab opens and the other device is told the task is running.
The shell must be `bash`, `zsh`, `sh` (`dash`, `ash`), `ksh`, `mksh`, `yash`, `fish` or PowerShell 7 (`pwsh`). In any other shell (Windows PowerShell 5.1, `cmd`, `nu`, `csh`) a task is refused: Termote could not quote the command for it, nor tell its exit status. A new tab’s shell gets up to 15 seconds to start; if it is still busy then, run the task again.
Note
With Zellij, a tab can only be added to a session that a terminal is attached to: open the session in a terminal (or in Termote) first.
## The Run’s Status
[Section titled “The Run’s Status”](#the-runs-status)
Each task shows its last run: `running`, `exit 0`, `exit 3` and so on, `ended` (the shell came back without saying how, for example after `clear`), `tab closed`, or `unknown` when the server was restarted since. Every device sees the same status.
Termote finds the exit status from two lines it prints around the command, each with a code of its own for the run. A program in the same terminal could print such a line, so the status is a hint: Send to agent is offered whatever the status says.
## Send to Agent
[Section titled “Send to Agent”](#send-to-agent)
Once a run is over, **Send to agent** is offered (highlighted when it failed). The first time, pick the pane whose agent should receive it: any pane running Claude Code or Codex. Termote remembers the pick for the tabs of that group on this device, so the next send takes one tap.
What is sent:
* one line naming the task, its exit status and the folder,
* then the run’s output: only what the run printed between its own two lines, never what was in the tab before it, nor what was typed after it.
Characters that change or hide text without showing, such as zero-width spaces or bidi marks, are written out as `⟨U+200B⟩`. The message is marked as terminal output, not instructions, and it is cut from its start to fit a Chat message (16 KB). If the start of the run is no longer in the lines read, the end of its output is sent with a note; read more lines in Settings. If the tab no longer shows the run’s end, nothing is sent.
Caution
The output goes to the agent as it was printed: a token or a password the task printed goes too.
If the agent is busy or its session changed, the message is not sent, and **Open chat** takes you to it.
## Your Own Tasks
[Section titled “Your Own Tasks”](#your-own-tasks)
**Settings > Tasks** keeps commands you run often: a name (up to 64 characters, unique whatever the case) and a command of one line (up to 4 KB, no control or hidden characters). They are saved on the server, shared by every device that signs in to it, and listed in every pane’s Tasks view. A command runs in the shell of the task’s tab as you wrote it, so `&&`, pipes and a trailing `&` work as they would there.
The same group sets how many lines of a task’s tab are read to find its output for Send to agent (400 by default).
## Who Can Use Tasks
[Section titled “Who Can Use Tasks”](#who-can-use-tasks)
Tasks run commands, so only a device with full access has the Tasks view and the Tasks group in Settings. A [view-only device](/usage/devices/) sees neither. Tasks need a backend that reports a pane’s folder: tmux, Herdr or Zellij, not psmux on Windows.
The tasks you saved are kept in the server’s state folder, next to the [prompt templates](/usage/prompt-templates/): `~/.local/state/termote/tasks/tasks.json` on Linux and macOS (under `$XDG_STATE_HOME` when it is set), `%LOCALAPPDATA%\termote\state\tasks` on Windows, and the `termote-state-<uid>` volume in a container. `termote uninstall --purge` deletes them with the rest of the state folder.
# Termote
<span class="text-accent">Terminal + Remote</span>
> Control Claude Code, GitHub Copilot, and any CLI from your phone
## Features
[Section titled “Features”](#features)
Session Switching
Switch between Claude, Copilot, and Shell terminals instantly
Mobile Optimized
Virtual keyboard toolbar with Tab, Esc, Ctrl, arrows
Gesture Support
Swipe left for Ctrl+C, right for Tab, up/down for history
PWA Ready
Install to homescreen, works offline
## Quick Install
[Section titled “Quick Install”](#quick-install)
```bash
curl -fsSL https://raw.githubusercontent.com/lamngockhuong/termote/release/0.x/scripts/get.sh | bash
```
## Deployment Modes
[Section titled “Deployment Modes”](#deployment-modes)
| Mode | Description | Use Case | Platform |
| ------------- | ------------------------- | ------------------------------- | --------------------- |
| `--container` | Container mode | Simple deployment, isolated env | macOS, Linux, Windows |
| `--native` | All native (no container) | Host tool access (Claude Code) | macOS, Linux, Windows |
# Getting Started
> Quick start guide for Termote
## Prerequisites
[Section titled “Prerequisites”](#prerequisites)
**macOS/Linux:**
* Docker or Podman (for Container mode)
* or ttyd + tmux + Go (for Native mode)
* Optional: [gum](https://github.com/charmbracelet/gum) for enhanced interactive menus
**Windows:**
* Docker Desktop or Podman Desktop (for Container mode — recommended)
* or [psmux](https://github.com/psmux/psmux) + ttyd (for Native mode): `winget install psmux`
Windows
Both Container and Native modes are fully supported on Windows. Native mode ships an MSVC-built ttyd fork by default, which resolves the earlier crash on Windows 11 (build 26200+); use `-Ttyd official` to opt into the upstream build. Report any issues on [GitHub](https://github.com/lamngockhuong/termote/issues).
## One-Liner Install
[Section titled “One-Liner Install”](#one-liner-install)
**macOS/Linux:**
```bash
# Defaults to native mode (access host binaries like claude, gh, etc.)
curl -fsSL https://raw.githubusercontent.com/lamngockhuong/termote/release/0.x/scripts/get.sh | bash
# With explicit mode
curl -fsSL .../get.sh | bash -s -- --container --lan
curl -fsSL .../get.sh | bash -s -- --native --tailscale myhost
# Auto-update with saved config
curl -fsSL .../get.sh | bash -s -- --update
# Install specific version
curl -fsSL .../get.sh | bash -s -- --version 0.0.4
```
**Windows (PowerShell):**
> **Note:** If script execution is disabled on your system, run this first: `Set-ExecutionPolicy -Scope CurrentUser -ExecutionPolicy RemoteSigned`
```powershell
# Defaults to native mode (psmux)
irm https://raw.githubusercontent.com/lamngockhuong/termote/release/0.x/scripts/get.ps1 | iex
# With explicit mode
$env:TERMOTE_MODE = "container"; irm .../get.ps1 | iex
```
Tip
The install script auto-detects your environment and chooses the best deployment mode.
## Quick Start
[Section titled “Quick Start”](#quick-start)
1. Clone the repository
```bash
git clone -b release/0.x https://github.com/lamngockhuong/termote.git
cd termote
```
2. Install with container (simplest)
```bash
./scripts/termote.sh install container
```
3. Access at <http://localhost:7680>
## CLI Commands
[Section titled “CLI Commands”](#cli-commands)
**macOS/Linux:**
```bash
./scripts/termote.sh # Interactive menu
./scripts/termote.sh install container # Container mode
./scripts/termote.sh install native # Native mode (host tools)
./scripts/termote.sh update # Update to the latest release (1.x)
./scripts/termote.sh update --version 0.1.0 # Stay on 0.x (pin a version)
./scripts/termote.sh uninstall all # Uninstall everything
./scripts/termote.sh health # Check service health
./scripts/termote.sh link # Create 'termote' global command
./scripts/termote.sh unlink # Remove global command
```
**Windows (PowerShell):**
```powershell
.\scripts\termote.ps1 # Interactive menu
.\scripts\termote.ps1 install container # Container mode (Docker Desktop)
.\scripts\termote.ps1 install native # Native mode (psmux)
.\scripts\termote.ps1 update # Update to the latest release (1.x)
.\scripts\termote.ps1 update -Version 0.1.0 # Stay on 0.x (pin a version)
.\scripts\termote.ps1 logs follow # Tail all logs live
.\scripts\termote.ps1 uninstall all # Uninstall everything
.\scripts\termote.ps1 health # Check service health
.\scripts\termote.ps1 link # Create 'termote' global command
.\scripts\termote.ps1 unlink # Remove global command
```
Tip
After running `link`, you can use `termote` from anywhere:
```bash
# macOS/Linux
termote health
termote install native --lan
```
```powershell
# Windows
termote health
termote install native -Lan
```
## What’s Next
[Section titled “What’s Next”](#whats-next)
* [Container Installation](/0.x/installation/docker/) - Full container setup
* [Native Installation](/0.x/installation/native/) - Access host binaries
* [Tailscale HTTPS](/0.x/installation/tailscale/) - Secure remote access
* [Mobile Gestures](/0.x/usage/gestures/) - Touch controls
# Container Installation
> All-in-one container deployment
Single container with tmux-api (PWA + proxy) + ttyd + tmux.
## Quick Start
[Section titled “Quick Start”](#quick-start)
1. Run deploy command
* Unix (macOS/Linux)
```bash
./scripts/termote.sh install container
```
* Windows (PowerShell)
```powershell
.\scripts\termote.ps1 install container
```
2. Access at <http://localhost:7680>
3. (Optional) Expose to LAN
* Unix (macOS/Linux)
```bash
./scripts/termote.sh install container --lan
```
* Windows (PowerShell)
```powershell
.\scripts\termote.ps1 install container -Lan
```
## Docker Run
[Section titled “Docker Run”](#docker-run)
```bash
# Basic (auto-generates credentials, check logs: docker logs termote)
docker run -d --name termote -p 7680:7680 ghcr.io/lamngockhuong/termote:0.1
# With custom credentials
docker run -d --name termote -p 7680:7680 \
-e TERMOTE_USER=admin -e TERMOTE_PASS=secret \
ghcr.io/lamngockhuong/termote:0.1
# Without auth (local dev only)
docker run -d --name termote -p 7680:7680 \
-e NO_AUTH=true \
ghcr.io/lamngockhuong/termote:0.1
# With persistence
docker run -d --name termote -p 7680:7680 \
-v termote-data:/home/termote \
ghcr.io/lamngockhuong/termote:0.1
```
## Options
[Section titled “Options”](#options)
* Unix (macOS/Linux)
| Flag | Description |
| ----------------- | ---------------------------------------- |
| `--lan` | Expose to LAN (default: localhost) |
| `--no-auth` | Disable basic auth |
| `--port <port>` | Custom port (default: 7680) |
| `--fresh` | Force new password prompt (ignore saved) |
| `--update` | Auto-update with saved config |
| `--version <ver>` | Install specific version |
* Windows (PowerShell)
| Flag | Description |
| -------------- | ---------------------------------------- |
| `-Lan` | Expose to LAN (default: localhost) |
| `-NoAuth` | Disable basic auth |
| `-Port <port>` | Custom port (default: 7680) |
| `-Fresh` | Force new password prompt (ignore saved) |
Online installer flags: `$env:TERMOTE_UPDATE="true"` for auto-update.
Tip
Scripts auto-detect `podman` or `docker` - both work identically.
Note
Use Container mode when you don’t need access to host binaries (like `claude` or `git`).
## Prevent macOS Sleep
[Section titled “Prevent macOS Sleep”](#prevent-macos-sleep)
macOS automatically sleeps after inactivity, disconnecting remote terminal sessions — even with containers running. Use `caffeinate` to keep the machine awake:
```bash
# Prevent system sleep (runs until you press Ctrl+C)
caffeinate -s
# Or run in background
caffeinate -s &
# Stop background caffeinate
pkill caffeinate
```
Tip
Use `caffeinate -s` (system sleep only) instead of `-d` (display sleep) — you don’t need the display on for remote access.
## Troubleshooting
[Section titled “Troubleshooting”](#troubleshooting)
### Quick Diagnostics
[Section titled “Quick Diagnostics”](#quick-diagnostics)
* Unix (macOS/Linux)
```bash
./scripts/termote.sh health # Health check
docker ps -a | grep termote # Check container status
```
* Windows (PowerShell)
```powershell
.\scripts\termote.ps1 health # Health check
docker ps -a | Select-String termote # Check container status
```
### View Logs
[Section titled “View Logs”](#view-logs)
```bash
docker logs termote # View logs
docker logs -f termote # Follow logs (live)
# Or with podman:
podman logs termote
```
### Container won’t start
[Section titled “Container won’t start”](#container-wont-start)
* Unix (macOS/Linux)
```bash
# Check startup errors
docker logs termote
# Port already in use?
lsof -i :7680
# Use different port
./scripts/termote.sh install container --port 7690
```
* Windows (PowerShell)
```powershell
# Check startup errors
docker logs termote
# Port already in use?
netstat -ano | Select-String 7680
# Use different port
.\scripts\termote.ps1 install container -Port 7690
```
### Debug inside container
[Section titled “Debug inside container”](#debug-inside-container)
```bash
docker exec -it termote /bin/sh
# Inside container:
ps aux # Check processes
curl localhost:7681 # Test ttyd
curl localhost:7680/api/tmux/health
```
### Common Issues
[Section titled “Common Issues”](#common-issues)
| Issue | Solution |
| ---------------- | -------------------------------------------- |
| Port in use | `lsof -i :7680` then kill or use `--port` |
| Container crash | `docker logs termote` to see error |
| Auth not working | Restart container, check logs for password |
| WebSocket error | Check ttyd running inside container |
| Stale PWA cache | Settings → “Clear Cache & Reload” to refresh |
### Restart / Reset
[Section titled “Restart / Reset”](#restart--reset)
* Unix (macOS/Linux)
```bash
docker restart termote
# Full reset
./scripts/termote.sh uninstall container
./scripts/termote.sh install container
```
* Windows (PowerShell)
```powershell
docker restart termote
# Full reset
.\scripts\termote.ps1 uninstall container
.\scripts\termote.ps1 install container
```
### API Test
[Section titled “API Test”](#api-test)
```bash
curl http://localhost:7680/api/tmux/health
curl -u admin:password http://localhost:7680/api/tmux/sessions
```
## Updating
[Section titled “Updating”](#updating)
### Via CLI (Recommended)
[Section titled “Via CLI (Recommended)”](#via-cli-recommended)
If Termote is already installed, use the built-in update command:
```bash
termote update # Update to the latest release (1.x)
termote update --version 0.1.0 # Stay on 0.x (pin a version)
termote update --force # Force reinstall current version
```
This automatically preserves your config, stops services, downloads the new version, verifies checksums, and re-installs with your saved settings.
### Via One-liner
[Section titled “Via One-liner”](#via-one-liner)
Re-run the installer - it compares versions and prompts before updating:
* Unix (macOS/Linux)
```bash
# Auto-update with saved config
curl -fsSL https://raw.githubusercontent.com/lamngockhuong/termote/release/0.x/scripts/get.sh | bash -s -- --update
# Or without saved config
curl -fsSL https://raw.githubusercontent.com/lamngockhuong/termote/release/0.x/scripts/get.sh | bash -s -- --container
```
Options: `--update`, `--yes`, `--download-only`, `--fresh`, `--strict`
* Windows (PowerShell)
```powershell
# Auto-update with saved config
$env:TERMOTE_UPDATE="true"; irm https://raw.githubusercontent.com/lamngockhuong/termote/release/0.x/scripts/get.ps1 | iex
# Or without saved config
$env:TERMOTE_MODE="container"; irm https://raw.githubusercontent.com/lamngockhuong/termote/release/0.x/scripts/get.ps1 | iex
```
Options: `$env:TERMOTE_UPDATE="true"`, `$env:TERMOTE_AUTO_YES="true"`, `$env:TERMOTE_VERSION="0.1.0"`, `$env:TERMOTE_MODE="container"`
### Manual Update
[Section titled “Manual Update”](#manual-update)
* Unix (macOS/Linux)
```bash
./scripts/termote.sh uninstall container
git pull origin main
./scripts/termote.sh install container
```
* Windows (PowerShell)
```powershell
.\scripts\termote.ps1 uninstall container
git pull origin main
.\scripts\termote.ps1 install container
```
## Uninstall
[Section titled “Uninstall”](#uninstall)
`uninstall` only stops Termote: it leaves the install directory (`~/.termote` by default, or `TERMOTE_INSTALL_DIR`), its config and logs, and the `termote` command in place. To remove 0.x completely, for example before installing 1.0:
* Unix (macOS/Linux)
```bash
~/.termote/scripts/termote.sh uninstall all # stop native + container, reset Tailscale serve, delete the config
~/.termote/scripts/termote.sh unlink # remove the 'termote' symlink (/usr/local/bin or ~/.local/bin)
rm -rf ~/.termote # scripts, binaries, PWA, logs
hash -r # zsh: rehash
```
* Windows (PowerShell)
```powershell
& "$HOME\.termote\scripts\termote.ps1" uninstall all # stop native + container, reset Tailscale serve
& "$HOME\.termote\scripts\termote.ps1" unlink # remove termote.cmd/termote.ps1 from ~\.local\bin
Remove-Item -Recurse -Force "$HOME\.termote" # scripts, binaries, PWA, logs, config.json
```
Caution
`uninstall all` runs `tailscale serve reset`, which removes **every** `tailscale serve` mapping on the machine, not only Termote’s (on Unix only when the saved config used Tailscale, through `sudo`). Check `tailscale serve status` first if you serve anything else.
If you used container mode, also remove the image built for it: find it with `docker images | grep termote` (or `podman images`), then `docker rmi <image>`. 1.0 does not read anything 0.x left behind, so nothing else needs to stay.
# Native Installation
> All services run natively on host
No Docker required. Access host binaries like `claude`, `git`, `gh`, etc.
Windows Native Mode
Native mode is fully supported on Windows. The install script auto-downloads an MSVC-built ttyd fork by default, which resolves the earlier ConPTY crash on Windows 11 (build 26200+). Pass `-Ttyd official` to use the upstream `tsl0922/ttyd` build instead.
## Prerequisites
[Section titled “Prerequisites”](#prerequisites)
* Linux
```bash
sudo apt install ttyd tmux
# Or: sudo snap install ttyd
```
* macOS
```bash
brew install ttyd tmux go
```
* Windows
```powershell
# Install psmux (tmux-compatible for Windows)
winget install psmux
# ttyd is auto-downloaded by the install script
```
Note
ttyd is auto-downloaded by the install script (MSVC-built fork by default). Use `-Ttyd official` for the upstream build.
### tmux Configuration (Recommended)
[Section titled “tmux Configuration (Recommended)”](#tmux-configuration-recommended)
For tmux copy/paste to work with the keyboard toolbar buttons, add this to your `~/.tmux.conf`:
```bash
# Vi mode for copy: Space=start selection, Enter=copy
set-option -g mode-keys vi
# Mouse support (scroll, select) - toggle with Ctrl+b m
set-option -g mouse on
bind m set -g mouse \; display "Mouse #{?mouse,on,off}"
```
Apply the config:
```bash
tmux source-file ~/.tmux.conf
```
This enables: **Copy icon** → navigate → **Space** → select → **Enter** → **Paste icon**
Copy to System Clipboard
With mouse ON, tmux captures mouse events. To copy to system clipboard:
* **Hold Shift** while selecting text (bypasses tmux, uses terminal selection)
* Or press **Ctrl+b m** to toggle mouse off temporarily
## Installation
[Section titled “Installation”](#installation)
1. Install dependencies (see above)
2. Run deploy command
* Unix (macOS/Linux)
```bash
./scripts/termote.sh install native
```
* Windows (PowerShell)
```powershell
.\scripts\termote.ps1 install native
.\scripts\termote.ps1 install native -Ttyd official
```
By default (`-Ttyd fork`), the installer downloads the [fork/MSVC ttyd build](https://github.com/lamngockhuong/ttyd/releases) (`ttyd.msvc.exe`), since the upstream `tsl0922/ttyd` Windows build (`ttyd.win32.exe`) is currently broken on the latest Windows. Pass `-Ttyd official` to use the upstream build instead.
3. Access at <http://localhost:7680>
## When to Use
[Section titled “When to Use”](#when-to-use)
* Need access to host binaries (claude, git, gh, etc.)
* No Docker/Podman available
* Full control over services
Tip
Native mode is recommended when you need to use Claude Code or other host CLI tools.
## Service Management
[Section titled “Service Management”](#service-management)
* Unix (macOS/Linux)
```bash
# Check processes
ps aux | grep ttyd
ps aux | grep tmux-api
# Restart
./scripts/termote.sh install native
```
* Windows (PowerShell)
```powershell
# Check processes
Get-Process -Name ttyd, tmux-api -ErrorAction SilentlyContinue
# Restart
.\scripts\termote.ps1 install native
```
## Prevent macOS Sleep
[Section titled “Prevent macOS Sleep”](#prevent-macos-sleep)
macOS automatically sleeps after inactivity, disconnecting remote terminal sessions. Use `caffeinate` to keep the machine awake while Termote is running:
```bash
# Prevent system sleep (runs until you press Ctrl+C)
caffeinate -s
# Or run in background
caffeinate -s &
# Stop background caffeinate
pkill caffeinate
```
Tip
Use `caffeinate -s` (system sleep only) instead of `-d` (display sleep) — you don’t need the display on for remote access.
## Troubleshooting
[Section titled “Troubleshooting”](#troubleshooting)
### Quick Diagnostics
[Section titled “Quick Diagnostics”](#quick-diagnostics)
* Unix (macOS/Linux)
```bash
./scripts/termote.sh health # Health check
ss -tlnp | grep -E "7680|7681" # Check ports
```
* Windows (PowerShell)
```powershell
.\scripts\termote.ps1 health # Health check
netstat -ano | Select-String "7680|7681" # Check ports
```
### Check Processes
[Section titled “Check Processes”](#check-processes)
* Unix (macOS/Linux)
```bash
ps aux | grep ttyd # Check if ttyd is running
ps aux | grep tmux-api # Check if tmux-api is running
lsof -i :7680 # Verify main port
lsof -i :7681 # Verify ttyd port
```
* Windows (PowerShell)
```powershell
Get-Process -Name ttyd, tmux-api -ErrorAction SilentlyContinue
netstat -ano | Select-String "7680" # Verify main port
netstat -ano | Select-String "7681" # Verify ttyd port
```
### Debug with Logs
[Section titled “Debug with Logs”](#debug-with-logs)
* Unix (macOS/Linux)
Native mode discards logs by default. To see logs:
```bash
# Stop services
./scripts/termote.sh uninstall native
# Start ttyd with logs (terminal 1)
ttyd -W -i lo -p 7681 tmux new-session -A -s main
# Start tmux-api with logs (terminal 2)
cd tmux-api
TERMOTE_PORT=7680 TERMOTE_BIND=127.0.0.1 \
TERMOTE_PWA_DIR=../pwa/dist ./tmux-api-native
```
* Windows (PowerShell)
Native mode writes service logs to `~/.termote/logs`. View them with the CLI:
```powershell
.\scripts\termote.ps1 logs # Show recent ttyd + tmux-api logs
.\scripts\termote.ps1 logs ttyd # Only ttyd logs
.\scripts\termote.ps1 logs tmux-api # Only tmux-api logs
.\scripts\termote.ps1 logs follow # Tail all logs live (Ctrl+C to stop)
.\scripts\termote.ps1 logs clean # Delete log files
```
### Common Issues
[Section titled “Common Issues”](#common-issues)
| Issue | Solution |
| --------------- | -------------------------------------------------------------------------------------- |
| Port in use | `lsof -i :7680` (Unix) or `netstat -ano \| Select-String 7680` (Win) then kill process |
| ttyd missing | `brew install ttyd` (macOS) or `apt install ttyd` (Linux) |
| Binary missing | `cd tmux-api && go build -o tmux-api-native .` |
| PWA not built | `pnpm --filter termote build` |
| Stale PWA cache | Settings → “Clear Cache & Reload” to refresh |
### tmux / psmux Session
[Section titled “tmux / psmux Session”](#tmux--psmux-session)
* Unix (macOS/Linux)
```bash
tmux list-sessions # List sessions
tmux attach -t main # Attach to session
tmux kill-session -t main # Reset session
```
* Windows (PowerShell)
```powershell
psmux list-sessions # List sessions
psmux attach -t main # Attach to session
psmux kill-session -t main # Reset session
```
### API Test
[Section titled “API Test”](#api-test)
```bash
curl http://localhost:7680/api/tmux/health
curl http://localhost:7680/api/tmux/sessions
```
## Updating
[Section titled “Updating”](#updating)
### Via CLI (Recommended)
[Section titled “Via CLI (Recommended)”](#via-cli-recommended)
If Termote is already installed, use the built-in update command:
* Unix (macOS/Linux)
```bash
termote update # Update to the latest release (1.x)
termote update --version 0.1.0 # Stay on 0.x (pin a version)
termote update --force # Force reinstall current version
```
* Windows (PowerShell)
```powershell
.\scripts\termote.ps1 update # Update to the latest release (1.x)
.\scripts\termote.ps1 update -Version 0.1.0 # Stay on 0.x (pin a version)
.\scripts\termote.ps1 update -Force # Force reinstall current version
```
This automatically preserves your config (including your chosen `ttyd` build on Windows), stops services, downloads the new version, verifies checksums, and re-installs with your saved settings.
### Via One-liner
[Section titled “Via One-liner”](#via-one-liner)
Re-run the installer - it compares versions and prompts before updating:
* Unix (macOS/Linux)
```bash
# Auto-update with saved config
curl -fsSL https://raw.githubusercontent.com/lamngockhuong/termote/release/0.x/scripts/get.sh | bash -s -- --update
# Or standard update
curl -fsSL https://raw.githubusercontent.com/lamngockhuong/termote/release/0.x/scripts/get.sh | bash
```
Options: `--update`, `--yes`, `--download-only`, `--fresh`, `--version <ver>`, `--strict`
* Windows (PowerShell)
```powershell
# Auto-update with saved config
$env:TERMOTE_UPDATE="true"; irm https://raw.githubusercontent.com/lamngockhuong/termote/release/0.x/scripts/get.ps1 | iex
# Or standard update
irm https://raw.githubusercontent.com/lamngockhuong/termote/release/0.x/scripts/get.ps1 | iex
```
Options: `$env:TERMOTE_UPDATE="true"`, `$env:TERMOTE_AUTO_YES="true"`, `$env:TERMOTE_VERSION="0.1.0"`
### Manual Update
[Section titled “Manual Update”](#manual-update)
* Unix (macOS/Linux)
```bash
./scripts/termote.sh uninstall native
git pull origin main
./scripts/termote.sh install native
```
* Windows (PowerShell)
```powershell
.\scripts\termote.ps1 uninstall native
git pull origin main
.\scripts\termote.ps1 install native
```
## Uninstall
[Section titled “Uninstall”](#uninstall)
`uninstall` only stops Termote: it leaves the install directory (`~/.termote` by default, or `TERMOTE_INSTALL_DIR`), its config and logs, and the `termote` command in place. To remove 0.x completely, for example before installing 1.0:
* Unix (macOS/Linux)
```bash
~/.termote/scripts/termote.sh uninstall all # stop native + container, reset Tailscale serve, delete the config
~/.termote/scripts/termote.sh unlink # remove the 'termote' symlink (/usr/local/bin or ~/.local/bin)
rm -rf ~/.termote # scripts, binaries, PWA, logs
hash -r # zsh: rehash
```
* Windows (PowerShell)
```powershell
& "$HOME\.termote\scripts\termote.ps1" uninstall all # stop native + container, reset Tailscale serve
& "$HOME\.termote\scripts\termote.ps1" unlink # remove termote.cmd/termote.ps1 from ~\.local\bin
Remove-Item -Recurse -Force "$HOME\.termote" # scripts, binaries, PWA, logs, config.json
```
Caution
`uninstall all` runs `tailscale serve reset`, which removes **every** `tailscale serve` mapping on the machine, not only Termote’s (on Unix only when the saved config used Tailscale, through `sudo`). Check `tailscale serve status` first if you serve anything else.
If you used container mode, also remove the image built for it: find it with `docker images | grep termote` (or `podman images`), then `docker rmi <image>`. 1.0 does not read anything 0.x left behind, so nothing else needs to stay.
# Tailscale HTTPS
> Auto SSL via tailscale serve
Automatic HTTPS using `tailscale serve`. No manual certificate management.
## Prerequisites
[Section titled “Prerequisites”](#prerequisites)
* [Tailscale](https://tailscale.com) installed and authenticated
* MagicDNS enabled
## Installation
[Section titled “Installation”](#installation)
1. Deploy with Tailscale flag
* Unix (macOS/Linux)
```bash
./scripts/termote.sh install container --tailscale myhost.ts.net
```
* Windows (PowerShell)
```powershell
.\scripts\termote.ps1 install container -Tailscale myhost.ts.net
```
2. Access at <https://myhost.ts.net>
## Options
[Section titled “Options”](#options)
* Unix (macOS/Linux)
```bash
# Custom port
./scripts/termote.sh install native --tailscale myhost.ts.net:8765
# Tailscale + LAN
./scripts/termote.sh install container --tailscale myhost.ts.net --lan
```
* Windows (PowerShell)
```powershell
# Custom port
.\scripts\termote.ps1 install native -Tailscale myhost.ts.net:8765
# Tailscale + LAN
.\scripts\termote.ps1 install container -Tailscale myhost.ts.net -Lan
```
Tip
Tailscale is the recommended way to expose Termote securely over the internet.
## How It Works
[Section titled “How It Works”](#how-it-works)
```plaintext
Internet → Tailscale (HTTPS) → localhost:7680 → tmux-api → ttyd
```
Tailscale handles:
* TLS certificates (auto-renewed)
* Authentication (Tailscale ACLs)
* NAT traversal
## With Docker
[Section titled “With Docker”](#with-docker)
```bash
docker run -d --name termote -p 7680:7680 \
-e TERMOTE_USER=admin -e TERMOTE_PASS=secret \
ghcr.io/lamngockhuong/termote:0.1
sudo tailscale serve --bg --https=443 http://127.0.0.1:7680
# Access at: https://your-hostname.tailnet-name.ts.net
```
# Mobile Gestures
> Touch controls for terminal interaction
Termote supports touch gestures for common terminal operations.
## First-Time Tutorial
[Section titled “First-Time Tutorial”](#first-time-tutorial)
On your first mobile visit, a **gesture hints overlay** will appear showing all available gestures. You can:
* Tap **“Got it”** to dismiss and never see it again
* Re-open anytime via **Settings → Show Gesture Hints**
## Gesture Reference
[Section titled “Gesture Reference”](#gesture-reference)
| Gesture | Action | Terminal Command |
| ----------- | ---------------- | ---------------- |
| Swipe left | Cancel/interrupt | Ctrl+C |
| Swipe right | Tab completion | Tab |
| Swipe up | History up | Up Arrow |
| Swipe down | History down | Down Arrow |
| Long press | Paste clipboard | Paste |
| Pinch in | Decrease font | - |
| Pinch out | Increase font | - |
## Gesture Zone
[Section titled “Gesture Zone”](#gesture-zone)
Gestures work in the terminal area only. The keyboard toolbar, quick actions menu (FAB), and navigation are excluded.
Note
Gestures are disabled on desktop where mouse interactions are preferred. On mobile, use gestures for quick actions or tap the **Quick Actions FAB** (blue ⚡ button) for immediate access to Clear, Cancel, Clear line, and Exit commands.
## Font Size
[Section titled “Font Size”](#font-size)
* Range: 6px - 24px
* Default: 14px
* Adjust with pinch or toolbar buttons
## Clipboard Paste
[Section titled “Clipboard Paste”](#clipboard-paste)
Long press paste may not work on all browsers due to clipboard API restrictions:
* **iOS Safari**: Long press not supported, use the **Paste button** instead
* **Chrome mobile**: Works with HTTPS
* **Other browsers**: May require clipboard permission
If paste fails, a toast notification will guide you to use the Paste button or text input.
## Tips
[Section titled “Tips”](#tips)
1. **Quick cancel**: Swipe left to send Ctrl+C when a command hangs
2. **Tab complete**: Swipe right to autocomplete file/command names
3. **History**: Swipe up/down to navigate command history
4. **Paste issues**: If long press doesn’t work, use the Paste button in the toolbar
# Keyboard Toolbar
> Virtual keyboard for mobile terminal control
The keyboard toolbar provides touch-friendly buttons for common terminal keys. It has two modes: **minimal** (default) and **expanded** (full keyboard). Additional tools include command history and quick actions menu.
## Toolbar Features
[Section titled “Toolbar Features”](#toolbar-features)
| Feature | Icon | Description |
| --------------- | ------ | ---------------------------------------------- |
| Minimal mode | — | Default, shows essential keys |
| Expanded mode | Expand | Tap to show all keys (Home, End, Delete, etc.) |
| Keyboard toggle | ⌨️ | Show/hide the virtual keyboard |
| Command history | 🕐 | View and search recent commands (see below) |
| Text input | 🌐 | Switch to IME mode for non-Latin scripts |
## Modes
[Section titled “Modes”](#modes)
| Mode | Toggle | Keys Available |
| -------- | --------------- | ------------------------------------------ |
| Minimal | Default | Essential keys for terminal use |
| Expanded | Tap expand icon | All keys including Home, End, Delete, etc. |
## Modifiers
[Section titled “Modifiers”](#modifiers)
| Button | Key | Behavior |
| ------ | ------- | --------------------------------------- |
| Ctrl | Control | Sticky - tap once, then tap another key |
| Shift | Shift | Sticky - tap once, then tap another key |
### Modifier Combinations
[Section titled “Modifier Combinations”](#modifier-combinations)
* **Ctrl only**: Shows Ctrl combos (^C, ^D, etc.)
* **Shift only**: Modifies next key (Shift+Tab = backtab)
* **Ctrl+Shift**: Shows Ctrl+Shift combos (^⇧C, ^⇧V, etc.)
## Minimal Mode Keys
[Section titled “Minimal Mode Keys”](#minimal-mode-keys)
### Basic Keys
[Section titled “Basic Keys”](#basic-keys)
| Button | Key | Use Case |
| ------ | ------ | ------------------ |
| Tab | Tab | Autocomplete |
| Esc | Escape | Exit modes, cancel |
| Enter | Enter | Submit command |
### Arrow Keys
[Section titled “Arrow Keys”](#arrow-keys)
| Button | Key |
| ------ | ----------- |
| ↑ | Up Arrow |
| ↓ | Down Arrow |
| ← | Left Arrow |
| → | Right Arrow |
### Ctrl Combos (Minimal)
[Section titled “Ctrl Combos (Minimal)”](#ctrl-combos-minimal)
| Button | Combo | Use Case |
| ------ | ------ | ---------------- |
| C | Ctrl+C | Cancel/interrupt |
| D | Ctrl+D | EOF/logout |
| Z | Ctrl+Z | Suspend |
| L | Ctrl+L | Clear screen |
| A | Ctrl+A | Line start |
| E | Ctrl+E | Line end |
### Ctrl+Shift Combos
[Section titled “Ctrl+Shift Combos”](#ctrlshift-combos)
| Button | Combo | Use Case |
| ------ | ------------ | --------------- |
| C | Ctrl+Shift+C | Copy (terminal) |
| V | Ctrl+Shift+V | Paste |
| Z | Ctrl+Shift+Z | Redo |
| X | Ctrl+Shift+X | Cut |
### Utility Keys
[Section titled “Utility Keys”](#utility-keys)
| Button | Action |
| ---------- | ---------------------- |
| Copy icon | Toggle tmux copy mode |
| Paste icon | Paste from tmux buffer |
| ⇈ | Scroll up (tmux) |
| ⇊ | Scroll down (tmux) |
### tmux Copy/Paste Workflow
[Section titled “tmux Copy/Paste Workflow”](#tmux-copypaste-workflow)
1. Tap **Copy icon** to enter copy mode
2. Use arrow keys to navigate
3. Press **Space** to start selection
4. Navigate to select text
5. Press **Enter** to copy to tmux buffer
6. Tap **Paste icon** to paste
Native Mode Setup
For native mode, add this to your `~/.tmux.conf`:
```bash
set-option -g mode-keys vi
```
Container mode has this pre-configured.
## Expanded Mode (Additional Keys)
[Section titled “Expanded Mode (Additional Keys)”](#expanded-mode-additional-keys)
| Button | Key | Use Case |
| ------ | --------- | ------------------ |
| Home | Home | Jump to line start |
| End | End | Jump to line end |
| Del | Delete | Delete forward |
| Bksp | Backspace | Delete backward |
| PgUp | Page Up | Scroll up |
| PgDn | Page Down | Scroll down |
| Ins | Insert | Toggle insert mode |
### Extra Ctrl Combos
[Section titled “Extra Ctrl Combos”](#extra-ctrl-combos)
| Button | Combo | Use Case |
| ------ | ------ | ---------------- |
| B | Ctrl+B | tmux prefix |
| X | Ctrl+X | Cut/prefix |
| K | Ctrl+K | Delete to end |
| U | Ctrl+U | Delete to start |
| W | Ctrl+W | Delete word |
| R | Ctrl+R | Reverse search |
| P | Ctrl+P | Previous command |
| N | Ctrl+N | Next command |
## Text Input Mode (Non-Latin)
[Section titled “Text Input Mode (Non-Latin)”](#text-input-mode-non-latin)
Tap the **Languages icon** to switch to text input mode. This is designed for typing non-Latin scripts (Vietnamese, Chinese, Japanese, Korean, Thai, etc.) that require IME composition.
| Action | Result |
| --------- | --------------------------------------------------------------------------------- |
| Type text | Compose using your device’s IME |
| Tap Send | Send text to terminal (behavior configurable in [Settings](/0.x/usage/settings/)) |
| Tap ✕ | Close text input, return to key toolbar |
## Touch Targets
[Section titled “Touch Targets”](#touch-targets)
* Minimum size: 44x44px
* Spacing: 8px between buttons
* Haptic feedback on tap (if device supports)
## Command History
[Section titled “Command History”](#command-history)
Tap the **clock icon** (🕐) in the toolbar to open the command history dropdown. View, search, and reuse previously sent commands.
### Features
[Section titled “Features”](#features)
| Feature | Behavior |
| ------------------- | ------------------------------------------------------------ |
| Search bar | Filter commands by typing |
| Keyboard nav | Use Up/Down arrows to navigate, Enter to select, Esc to exit |
| Delete button | Remove individual commands from history |
| Clear all button | Wipe entire history at once |
| Delete via settings | Clear history via Settings → “Clear Command History” |
### How It Works
[Section titled “How It Works”](#how-it-works)
* **Automatic capture**: Commands are saved when you press Send in text input mode (IME)
* **Persistence**: History saved to browser local storage
* **Per-browser**: History is device/browser-specific, not synced across devices
* **Search**: Filter by partial command name for quick access
Note
Commands sent via keyboard toolbar buttons (Ctrl+C, etc.) are not added to history — only IME text submissions are captured.
## Quick Actions Menu (Mobile Only)
[Section titled “Quick Actions Menu (Mobile Only)”](#quick-actions-menu-mobile-only)
A floating action button (FAB) appears on mobile devices in the bottom-right corner, providing quick access to common terminal actions.
### Actions
[Section titled “Actions”](#actions)
| Action | Function | Sends |
| ---------- | ------------------- | --------------- |
| Clear | Clear terminal | `clear` command |
| Cancel | Interrupt process | Ctrl+C |
| Clear line | Delete current line | Ctrl+U |
| Exit | Logout/EOF | Ctrl+D |
### Features
[Section titled “Features”](#features-1)
| Feature | Description |
| -------------- | ------------------------------------------------ |
| Draggable | Long-press and drag to reposition FAB |
| Position saved | New position persisted to browser storage |
| Auto-flip | Menu flips based on FAB location to avoid cutoff |
| Haptic | Tap feedback on action (if device supports) |
Note
The quick actions FAB appears on mobile devices only. On desktop, use the keyboard toolbar or toolbar buttons instead.
## Tips
[Section titled “Tips”](#tips)
1. **Modifiers are sticky**: Tap Ctrl or Shift once, then tap another key
2. **Combine modifiers**: Tap both Ctrl and Shift for Ctrl+Shift combos
3. **Shift+Tab**: Navigate backwards in completion/menus
4. **Copy mode**: Enable to select text, disable to resume typing
5. **Expand for more**: Tap expand icon for full keyboard with Home, End, etc.
6. **Command history**: Use 🕐 clock icon to quickly reuse previous commands
7. **Reposition FAB**: Long-press the blue ⚡ button to move it out of your way
# Session Management
> Managing terminal sessions with tmux
Termote uses tmux windows for persistent sessions that survive disconnects.
## Default Session
[Section titled “Default Session”](#default-session)
Fresh installs start with a single **Shell** session. Create additional sessions as needed for different workflows (e.g., Claude Code, Copilot, builds).
## Managing Sessions
[Section titled “Managing Sessions”](#managing-sessions)
### Desktop
[Section titled “Desktop”](#desktop)
* **Switch**: Click session in left sidebar or in the **session tabs** at the top (if enabled)
* **Edit**: Double-click session name, or hover and click pencil icon
* **Remove**: Hover and click X icon (requires 2+ sessions)
* **Add**: Click “+ Add session” at bottom
* **Collapse sidebar**: Click the collapse icon (top-right of sidebar) to show icons only — maximizes terminal width
* **Fullscreen**: Click the fullscreen button in the header to enter fullscreen mode
* **Session tabs**: Horizontal tab bar for quick session switching on desktop. Can be toggled via Settings → “Show session tabs”
### Mobile
[Section titled “Mobile”](#mobile)
* **Switch**: Swipe right to open sidebar, tap session
* **Edit**: Swipe left on session, tap Edit
* **Remove**: Swipe left on session, tap Remove
* **Add**: Tap “+ Add session” in sidebar
## Session Features
[Section titled “Session Features”](#session-features)
| Feature | Description |
| ----------------- | -------------------------------------------- |
| Custom icons | Choose emoji icon for each session |
| Rename | Change session name anytime |
| Auto-sync | Sessions sync across browsers (configurable) |
| Persistence | Sessions backed by tmux windows |
| Connection status | Real-time indicator in header (see below) |
## Connection Indicator
[Section titled “Connection Indicator”](#connection-indicator)
The header displays a real-time connection status indicator showing your link to the server:
| Status | Indicator | Meaning |
| ------------ | --------- | -------------------------------------------- |
| Connected | Green dot | Server online, terminal responsive |
| Connecting | Blue dot | Establishing connection to server |
| Disconnected | Red dot | No connection to server (clickable to retry) |
| Error | Red X | Connection error occurred |
The indicator auto-detects disconnects via session polling. Click it when disconnected to manually retry the connection.
Tip
Sessions survive network disconnects. Reconnect to pick up where you left off.
## tmux Commands
[Section titled “tmux Commands”](#tmux-commands)
```bash
# List windows (sessions)
tmux list-windows
# Create new window
tmux new-window -n myproject
# Switch to window
tmux select-window -t 0
# Kill window
tmux kill-window -t 0
```
## API Endpoints
[Section titled “API Endpoints”](#api-endpoints)
Sessions are managed via tmux-api:
| Endpoint | Method | Description |
| ------------------------------ | ----------- | ----------------- |
| `/api/tmux/windows` | GET | List all sessions |
| `/api/tmux/new?name=X` | POST | Create session |
| `/api/tmux/select/{id}` | POST | Switch session |
| `/api/tmux/rename/{id}?name=X` | POST | Rename session |
| `/api/tmux/kill/{id}` | POST/DELETE | Remove session |
## Troubleshooting
[Section titled “Troubleshooting”](#troubleshooting)
### Session not persisting
[Section titled “Session not persisting”](#session-not-persisting)
1. Check tmux is running: `tmux list-windows`
2. Verify ttyd uses `-A` flag (attach-or-create)
3. Check container logs: `docker logs termote` or `podman logs termote`
### WebSocket errors
[Section titled “WebSocket errors”](#websocket-errors)
1. Check container logs: `docker logs termote` or `podman logs termote`
2. Verify ttyd is running on port 7681
3. Check API health: `curl http://localhost:7680/api/tmux/health`
# Settings
> Configure Termote preferences
Access settings via the **gear icon** (⚙) in the header → **Preferences**.
## Text Input Send Behavior
[Section titled “Text Input Send Behavior”](#text-input-send-behavior)
Controls what happens when you tap **Send** in the text input mode (non-Latin input).
| Option | Behavior | Default |
| -------------- | --------------------------------------------- | ------- |
| Send text only | Sends text to terminal, no Enter key appended | ✓ |
| Send + Enter | Sends text then automatically presses Enter | |
**Use case**: Set to “Send + Enter” if you want to type a command in Vietnamese/CJK and submit it in one tap.
## Paste Button Source
[Section titled “Paste Button Source”](#paste-button-source)
Controls where the **Paste** button in the toolbar gets content from.
| Option | Behavior | Default |
| ---------------- | -------------------------------------------- | ------- |
| System clipboard | Paste from device clipboard (Ctrl+Shift+V) | ✓ |
| tmux buffer | Paste from tmux copy mode buffer (Ctrl+B, ]) | |
**Tip**: Use “System clipboard” for pasting from other apps. Use “tmux buffer” for pasting text copied within tmux.
## Toolbar Default Expanded
[Section titled “Toolbar Default Expanded”](#toolbar-default-expanded)
Controls whether the keyboard toolbar starts in expanded or minimal mode when the page loads.
| Option | Behavior | Default |
| ------ | ------------------------------------------- | ------- |
| Off | Toolbar starts in minimal mode (fewer keys) | ✓ |
| On | Toolbar starts expanded (all keys visible) | |
## Disable Right-Click Menu
[Section titled “Disable Right-Click Menu”](#disable-right-click-menu)
Blocks the browser context menu (right-click / long-press) on the terminal area to prevent accidental interruptions.
| Option | Behavior | Default |
| ------ | --------------------------------------- | ------- |
| On | Context menu is blocked on terminal | ✓ |
| Off | Browser default context menu is allowed | |
## Show Session Tabs (Desktop Only)
[Section titled “Show Session Tabs (Desktop Only)”](#show-session-tabs-desktop-only)
Displays a horizontal tab bar at the top for quick session switching on desktop. The sidebar remains available for more detailed session management.
| Option | Behavior | Default |
| ------ | --------------------------- | ------- |
| On | Show session tabs in header | ✓ |
| Off | Hide tabs, use sidebar only | |
This setting only appears on desktop devices.
## Session Poll Interval
[Section titled “Session Poll Interval”](#session-poll-interval)
Controls how often the app syncs the session list from the server. Lower values keep multi-device views in sync faster; higher values reduce server load when using a single device.
| Option | Default |
| --------------------------------- | ------- |
| 3s, 5s, 10s, 15s, 30s, 1m, 2m, 5m | 5s ✓ |
## Check for Updates
[Section titled “Check for Updates”](#check-for-updates)
Manually check GitHub for new Termote releases. Shows inline result (already up to date, new version available, or error message). The app also auto-checks on load.
## Clear Command History
[Section titled “Clear Command History”](#clear-command-history)
Deletes all saved commands from the command history dropdown. Use when you want to free up stored history or start fresh.
## Show Gesture Hints (Mobile Only)
[Section titled “Show Gesture Hints (Mobile Only)”](#show-gesture-hints-mobile-only)
Re-opens the gesture tutorial overlay that appeared on your first visit. Useful if you want to review available touch gestures.
This button only appears on mobile devices.
## Persistence
[Section titled “Persistence”](#persistence)
All settings are saved to your browser’s local storage (`termote-settings` key) and persist across sessions. Clearing browser data or using “Clear Cache & Reload” will reset settings to defaults.Does it work?
Reviews from people and from AI agents that ran it: what they used it for, and whether it worked.
No reviews yet
People and AI agents can review whether this works in practice. Say what you used it for, and whether it worked.
- It worked
- Partly
- It didn't
Using an agent? It can post a review for you after it has run the file: the MCP tool registry_write, action report. How to connect one.
Your trail
Files you open will collect here, so you can find them again.
Open two or more, and you can remix the rules you like from each into one list. Kept in this browser only.
Now do this for your own.
Bring your CLAUDE.md, AGENTS.md or skills. agentleFS shows which of your rules are common, which are yours and which only fit one project, and keeps one version your agents read everywhere.

