# Termish Web

**The browser window into your agents.** Termish Web is a web-based terminal manager built around [herdr](https://herdr.dev) — the runtime your coding agents live on. It renders terminals in the browser with xterm.js, connects to local and remote hosts, and turns herdr into something you can see, drive, and get notified from — anywhere with a browser.

> 🇨🇳 [中文文档](README.zh-CN.md) · [FAQ](FAQ.md) · [GitHub](https://github.com/baby2011/termish-web) · [Homepage](https://termish.dev/web)

> 📱 Also on your phone: [Termish mobile app](https://termish.dev) — Mosh-first
> SSH/Mosh terminal for Android · iOS · Desktop — the same herdr windows in
> your pocket · [GitHub](https://github.com/icelum/termish)

![Termish Web — the browser window into your agents](https://cdn.jsdelivr.net/npm/@termish/web/docs/screenshots/en.png)

## Why Termish Web?

Coding agents (Claude Code, Codex, …) run best on a real machine that stays on — your desktop, a NAS, a rented box. herdr is what keeps them running: it holds real terminal sessions open so the work survives the lid closing. The problem: herdr lives in a terminal, and terminals are where *you* are not.

Termish Web fixes that:

- **Your agents, from any browser.** Open herdr sessions — local or over SSH — and work with them from any device with a browser. Your browser is the client — nothing to install on the device you're viewing from.
- **See what they're doing.** A global agent view shows every agent across local and remote hosts — which project, which host, working or idle — updated in real time.
- **Drive them from the web.** Send instructions to an agent's pane, read its latest output, jump your terminal to its workspace.
- **Never miss a finish.** When an agent completes a long task, Termish Web raises a system notification — even from a background tab.
- **Show, don't tell.** Share a read-only view of any live session with a link — no account, no setup.

And when you just need a terminal, it's a full SSH/SFTP terminal manager too: persistent sessions, split workspaces, host management, audit log — no herdr required.

**On the go?** The [Termish mobile app](https://github.com/icelum/termish) is the
pocket side of the same sessions — Mosh keeps terminals alive through WiFi
switches and lock screens, with the same herdr windows and agent monitoring.

## What is herdr?

[herdr](https://herdr.dev) is the runtime your coding agents live on — an open-source terminal environment (Apache 2.0) that holds real terminal sessions open on your laptop, desktop, or a box you rent. Agents keep working when you close the lid, and you can attach again from anything with a keyboard. It detects and runs 20+ agent CLIs (Claude Code, Codex, …) and has a rich plugin ecosystem.

Termish Web integrates with herdr **through its public interfaces only**: the Unix-socket JSON-RPC API (`HERDR_SOCKET_PATH`) when running inside herdr, the `herdr agent list` CLI over SSH for remote hosts, and the iTerm2 progress protocol (OSC 9;4) that herdr uses to report agent state. herdr is a third-party project, not part of Termish Web.

## Quick start

### Run from npm (no build needed)

```bash
npm install -g @termish/web
# using a mirror registry (e.g. npmmirror) and the latest version is missing? install from the official registry:
npm install -g @termish/web --registry=https://registry.npmjs.org
termish-web            # starts on :8090 (auto-increments if the port is taken)
termish-web 9000       # custom port
```

> Requires Node.js >= 22.5 (uses the experimental `node:sqlite`).

Open http://localhost:8090. A "Local" host is auto-created on first start — click it to open a local herdr/terminal session immediately.

### Develop from source

```bash
pnpm install
pnpm dev               # server :8090 + client :5173
pnpm test              # backend core logic tests
pnpm typecheck         # type-check all packages
pnpm lint              # eslint
```

Open http://localhost:5173. Click **+** to add a host (address / auth / mode), then click a host to open a session tab.

### Connecting to herdr

- **Local**: run `termish-web` from inside a herdr terminal (or start a local host) — the backend picks up `HERDR_SOCKET_PATH` and the agent console lights up.
- **Remote**: add a host, choose **herdr mode**, and the remote `herdr agent list` CLI feeds remote agents into the view. If herdr isn't installed on the remote host, the UI offers a guided one-click install (TOFU-protected).

## Features

### Agent console — herdr in the browser

- **Global agent view**: every agent — from the local herdr socket, from remote hosts over SSH, and from terminal panes in this workspace — grouped by project, sorted by activity, with live working/idle status
- **Real-time status**: agent state changes stream over SSE (`/api/agents/stream`); a status bar shows "N agents working" at a glance
- **Drive agents from the web**: send text to an agent's pane (`pane.send_text`), read its recent output (`pane.read`), and click an agent to focus its workspace in your herdr terminal
- **Completion notifications**: working → idle transitions trigger a browser system notification (background tabs included), with a toggle in Settings
- **Remote agents**: herdr agents on other machines appear too — queried by SSH-executing `herdr agent list` on the remote host

### Terminal experience

- **Two connection modes**: `herdr` (exec herdr over SSH and drop straight into the TUI) and `ssh` (interactive shell) — both are "SSH + a launch command" under the hood
- **Persistent sessions**: SSH connections stay alive in the backend; refreshing/closing the browser only detaches, and reopening re-attaches to the same session — shell and herdr state are preserved
- **Auto-reconnect**: dropped SSH connections retry with exponential backoff (1s/2s/4s/8s) while keeping the session id and scrollback
- **Split workspaces**: a workspace (tab) holds one or more resizable panes (row / column) — terminals and SFTP mixed; switching workspaces never disconnects
- **Terminal niceties**: search (Ctrl/Cmd+Shift+F), select-to-copy, VS Code-style command palette (Ctrl/Cmd+Shift+P / F1), 5 terminal palettes + light/dark themes
- **OSC-aware**: responds to color queries (OSC 10/11/4/12) so TUI apps like herdr render correctly in the browser

### Mobile

- **Responsive layout**: on phones (≤768px) the sidebar collapses into a drawer, split panes stack vertically, and touch targets are enlarged
- **Terminal keybar**: touch devices get a two-row key toolbar (ported from the [Termish mobile app](https://github.com/icelum/termish) — install it for Mosh-roaming sessions in your pocket) — sticky Ctrl/Alt modifiers, one-tap ⌃C/⌃D/⌃L/⌃E, Tab, Enter, paste, and arrows that follow DECCKM (application cursor mode), so TUIs like herdr stay fully operable from a phone. A ⌨ toggle hides the on-screen keyboard to reclaim screen space
- **Installable PWA**: add to home screen for a standalone, app-like launch (web manifest + iOS meta included)
- **Notifications on the go**: agent-completion notifications reach a phone browser tab too

### Hosts & files

- **Host management**: add / edit / delete / group; auth supports password, private key, and SSH agent; one-click import from `~/.ssh/config`
- **Local host auto-created**: on first start a "Local" host is added automatically, using a **direct PTY** (node-pty, no sshd required) with `~/.ssh` private-key fallback to SSH loopback
- **SFTP file management**: browse / preview / edit / upload / download (streaming, binary-safe) / mkdir / rename / delete — drag & drop multi-file and directory upload. For local (direct-PTY) hosts, SFTP operates directly on the local filesystem — no sshd or authentication needed
- **Host key verification**: first connection shows the fingerprint (OpenSSH-style); trust-and-remember, with warnings on key changes; nothing is ever auto-trusted (TOFU everywhere, including herdr install)
- **IPv6 fallback**: dual-stack hostnames that fail over IPv4 automatically retry over IPv6
- **Live indicators**: a green dot marks hosts with an active session

### Safety & data

- **Token auth**: the backend auto-generates an access token; local browser fetches it once via `/api/bootstrap`; rotate it anytime from Settings → Security; pin a fixed one with `TERMISH_AUTH_TOKEN`
- **Credential encryption**: passwords and private keys are encrypted with AES-256-GCM; the master key lives in `master.key` (mode 600) next to the database
- **Default local-only**: listens on `127.0.0.1` unless you explicitly set `HOST=0.0.0.0` — see the Security section for the trade-offs
- **Audit log**: connection and file operations are recorded (`/api/audit-logs`, capped at 5000 entries)
- **SQLite storage**: hosts, settings, known hosts, and open workspaces persist to `~/.termish-web/termish.db`
- **Read-only sharing**: generate a share link for any live session — the viewer streams output over SSE with input disabled

## Screenshots

![SFTP file management](https://cdn.jsdelivr.net/npm/@termish/web/docs/screenshots/en-sftp.png)

![Split workspaces — terminals and SFTP side by side](https://cdn.jsdelivr.net/npm/@termish/web/docs/screenshots/en-split.png)

## Architecture

```
Browser (React 19 + Vite + xterm.js)
   ├── REST  /api/*   → hosts, settings, workspaces, audit log, agents, share
   ├── WS    /ws      → session attach / detach / input / output
   └── SSE   /api/agents/stream → live agent status (query-token auth)
Node backend (Koa + ws + ssh2 + node:sqlite)
   ├── SessionManager (persistent session pool; SSH connections decoupled from WebSocket)
   │    └── SSH ──→ remote host (herdr / shell)
   └── herdr integration
        ├── Unix socket JSON-RPC (HERDR_SOCKET_PATH) ──→ local herdr agents
        ├── SSH exec "herdr agent list" ──→ remote herdr agents
        └── OSC 9;4 progress detection ──→ agent working/idle in every pane
```

**Persistent sessions** are the core: `SshSession` holds a long-lived SSH connection and scrollback buffer; the browser subscribes to the data stream on attach (herdr replays the init prefix + triggers a resize redraw, ssh replays history) and detach never closes the SSH connection.

**Agent status flow**: the backend lists agents via the herdr socket (local) and over SSH (remote), a hub polls for changes (1.5s), and broadcasts only diffs over SSE. In parallel, every terminal pane decodes OSC 9;4 progress sequences from herdr to detect working → idle transitions — powering the status bar, the agent view, and completion notifications.

## Repository layout

```
apps/
  server/   Node.js + TS: Koa REST API + WebSocket + SessionManager + SQLite
  client/   React + Vite + TS: sidebar + tabs + terminal + theming
    components/ui/  shared base components (Modal / Button / IconButton / Segmented)
packages/
  shared/   shared protocol types (@termish/shared)
```

## Docker

One-command self-hosting (app + website):

```bash
docker compose up -d --build     # app on :8090, site on :8091
```

Or run just the app manually:

```bash
docker build -t termish-web .
docker run -d -p 8090:8090 -v termish-data:/app/data termish-web
```

In production the server is a single process that also serves the built frontend at http://localhost:8090. Data (SQLite + master key) persists in the `/app/data` volume.

The image keeps the **secure defaults**: `TERMISH_ALLOW_REMOTE_BOOTSTRAP` is **off**, so `/api/bootstrap` only serves loopback clients. Under Docker's default bridge networking the server sees the Docker gateway IP instead of the loopback address — if you need the first-visit token handoff, enable it explicitly and pin a token:

```bash
docker run -d -p 127.0.0.1:8090:8090 -v termish-data:/app/data \
  -e TERMISH_ALLOW_REMOTE_BOOTSTRAP=1 \
  -e TERMISH_AUTH_TOKEN=$(openssl rand -hex 32) termish-web
```

**Never combine `TERMISH_ALLOW_REMOTE_BOOTSTRAP=1` with a port published beyond loopback** (`-p 8090:8090`): anyone who can reach the port can claim the token with no credentials and read every host password/private key. If you must expose it, put a TLS reverse proxy with basic auth in front.

For development there is also a **test SSH host** (`docker/ssh-host`):

```bash
docker build -t termish-ssh-host docker/ssh-host
docker run -d -p 2222:22 --name termish-host termish-ssh-host
```

It runs Ubuntu with sshd + herdr preinstalled — connect to `127.0.0.1:2222` as `test` / `test1234` to exercise terminal, SFTP, host-key trust and reconnect flows against a real remote host.

## Run as a service

Termish Web runs in the foreground and shuts down gracefully on SIGINT/SIGTERM (sessions are closed cleanly). This is the **recommended shape for a service**: instead of a built-in `--daemon`, let your OS service manager own the lifecycle (auto-start on boot, restart on crash, log collection).

### macOS — launchd

Save as `~/Library/LaunchAgents/dev.termish.web.plist`, then `launchctl load ~/Library/LaunchAgents/dev.termish.web.plist`:

```xml
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
    <key>Label</key>
    <string>dev.termish.web</string>
    <key>ProgramArguments</key>
    <array>
        <string>/usr/bin/env</string>
        <string>termish-web</string>
    </array>
    <key>RunAtLoad</key>
    <true/>
    <key>KeepAlive</key>
    <true/>
    <key>StandardOutPath</key>
    <string>/Users/you/.termish-web/server.log</string>
    <key>StandardErrorPath</key>
    <string>/Users/you/.termish-web/server.log</string>
</dict>
</plist>
```

Logs: `tail -f ~/.termish-web/server.log`. Stop: `launchctl unload ~/Library/LaunchAgents/dev.termish.web.plist`.

### Linux — systemd

Save as `/etc/systemd/system/termish-web.service`, then `sudo systemctl enable --now termish-web`:

```ini
[Unit]
Description=Termish Web
After=network.target

[Service]
ExecStart=/usr/bin/env termish-web
Restart=on-failure
Environment=NODE_ENV=production

[Install]
WantedBy=multi-user.target
```

Logs: `journalctl -u termish-web -f`. Stop: `sudo systemctl stop termish-web`.

## Environment variables

| Variable | Default | Description |
|----------|---------|-------------|
| `PORT` | `8090` | Listen port (auto-increments when occupied) |
| `HOST` | `127.0.0.1` | Listen address (`0.0.0.0` to expose on the LAN) |
| `TERMISH_DB_PATH` | `~/.termish-web/termish.db` | SQLite database path |
| `TERMISH_CLIENT_DIST` | bundled `dist/public` | Frontend static asset directory |
| `TERMISH_AUTH_TOKEN` | auto-generated | Inject a fixed access token (Docker / multi-instance / LAN deployments) |
| `TERMISH_ALLOW_REMOTE_BOOTSTRAP` | unset (off) | Set to `1` to let non-loopback clients fetch the token via `/api/bootstrap` (needed under Docker bridge networking). Off by default — enable only when necessary, never on a port exposed beyond loopback, and always pin `TERMISH_AUTH_TOKEN` alongside |
| `HERDR_SOCKET_PATH` | inherited from env | Path to the herdr Unix socket; when set, the backend exposes the local agent console |

## Security

- **API/WS auth**: the backend auto-generates an access token; the local browser fetches it once via `/api/bootstrap` and caches it. Non-local requests are rejected unless `TERMISH_ALLOW_REMOTE_BOOTSTRAP=1` is set. Deployers can pin a token with `TERMISH_AUTH_TOKEN`, and rotate the generated one anytime from Settings → Security (the old token invalidates immediately).
- **Credential encryption**: passwords and private keys are encrypted with AES-256-GCM; the master key lives in `master.key` (mode 600) next to the database.
- **Default bind**: listens on `127.0.0.1` only. Set `HOST=0.0.0.0` explicitly if you need LAN access and understand the risk: **any client that obtains the token can read host credentials (passwords / private keys via `/api/hosts/:id`) and operate SFTP**. Pin a strong `TERMISH_AUTH_TOKEN` and rotate it if compromised.
- **Host list is redacted**: `/api/hosts` does not return plaintext credentials; they are fetched per-host only when connecting.
- **Host key trust is never auto-accepted**: every remote operation (terminal, SFTP, herdr install, remote agent query) requires the host key to be trusted first — first connection shows the fingerprint and remembers it on consent.

### Deployment checklist

- **Pin a token when exposed**: any client that reaches the port and obtains the token can read plaintext host credentials (`/api/hosts/:id`) and operate SFTP/terminals. Run with `-e TERMISH_AUTH_TOKEN=$(openssl rand -hex 32)` whenever the port is reachable beyond loopback.
- **Keep bootstrap loopback-only**: leave `TERMISH_ALLOW_REMOTE_BOOTSTRAP` unset unless first-visit token handoff is genuinely required (Docker bridge networking). Never combine it with a port published to the LAN/WAN — that combination lets anyone claim the token with no credentials.
- **Prefer loopback publishing**: `-p 127.0.0.1:8090:8090` + SSH tunnel, or a TLS reverse proxy with basic auth for remote access.
- **Rotate on suspicion**: Settings → Security rotates the generated token (old one invalidates immediately). If you pinned `TERMISH_AUTH_TOKEN`, replace it in the environment and restart.

## REST API

| Method | Path | Description |
|--------|------|-------------|
| GET / POST | `/api/hosts` | list / create hosts |
| GET / PUT / DELETE | `/api/hosts/:id` | get (with credentials) / update / delete host |
| POST | `/api/hosts/:id/touch` | update last-used time |
| POST | `/api/hosts/import-ssh-config` | batch-import hosts from `~/.ssh/config` |
| GET | `/api/hosts/:hostId/agents` | list herdr agents on a remote host (SSH exec) |
| GET / PUT | `/api/settings` | read / merge settings |
| GET / PUT | `/api/tabs` | read / replace open workspaces |
| GET / DELETE | `/api/known-hosts` | trusted host fingerprints |
| GET | `/api/audit-logs` | audit log entries |
| GET / POST | `/api/sftp/:id/:op` | SFTP ops (list/read/write/mkdir/rename/remove/download/upload) |
| GET | `/api/agents` | herdr agents from the local socket (available=false outside herdr) |
| GET | `/api/agents/stream` | SSE stream of agent status changes (query-token auth) |
| GET | `/api/agents/output` | recent output of a local herdr pane (`pane.read`) |
| POST | `/api/agents/send` | send text to a local herdr pane (`pane.send_text`) |
| POST | `/api/agents/focus` | focus a workspace in the local herdr terminal (`workspace.focus`) |
| POST | `/api/sessions/:sessionId/share` | create a read-only share link for a live session |
| GET | `/api/share/:token/stream` | SSE stream of a shared session (read-only, token-checked) |
| GET | `/api/bootstrap` | fetch auth token (localhost only by default) |
| GET | `/api/whoami` | current system user + agent availability |
| POST | `/api/token/rotate` | rotate the access token (old one invalidates immediately) |

## WebSocket protocol

JSON messages, shared between `apps/server` and `apps/client` via `@termish/shared`.

Client → server:

| type | Description |
|------|-------------|
| `session-create` | create a persistent session (host / auth / mode / command / size; `local: true` for direct PTY — ignores host/auth) |
| `session-attach` | attach to an existing session (restore on refresh) |
| `session-input` / `session-resize` | input / resize |
| `session-detach` / `session-close` | detach view / close session |
| `herdr-install` | install herdr on the remote host (or locally for PTY sessions) via the official script |
| `hostkey-response` | host key verification response |

Server → client:

| type | Description |
|------|-------------|
| `session-created` / `session-attached` | session ready (attach carries replay) |
| `session-data` / `session-closed` | terminal output / session closed |
| `session-reconnecting` | auto-reconnect progress |
| `herdr-install-output` / `herdr-install-result` | streaming install output / final result |
| `hostkey-verify` | host key verification request (fingerprint / algorithm / changed) |
| `error` | error message (with i18n `code` for the UI) |

## SSE streams

| Path | Description |
|------|-------------|
| `/api/agents/stream` | live agent status diffs (15s heartbeat; query-token auth since EventSource can't set headers) |
| `/api/share/:token/stream` | read-only output of a shared session (replays on connect) |

## Data model

- **hosts**: host config (name / address / auth / mode / herdr path / group / local connection type)
- **settings**: key-value (`theme`, `terminalTheme`, language, notifications)
- **known_hosts**: trusted fingerprints (`host:port` → SHA256 fingerprint)
- **open_tabs**: open workspaces (workspace id → panes; each pane maps to a session id / host / position / layout direction)
- **audit_logs**: connection and file-operation records (capped at 5000)

## See also

- [Termish mobile app](https://github.com/icelum/termish) — Mosh-first
  SSH/Mosh terminal for Android · iOS · Desktop: the same herdr windows in your
  pocket, surviving WiFi switches and roaming.

## Contributing

See [CONTRIBUTING.md](CONTRIBUTING.md). Please read the [Code of Conduct](CODE_OF_CONDUCT.md) and [Security Policy](SECURITY.md).

## Third-party notice

**herdr** is a third-party product by its own developers (https://herdr.dev). This project integrates with herdr solely through its public interfaces (Unix-socket JSON-RPC, `herdr agent list` CLI, and the OSC 9;4 progress protocol); herdr is not affiliated with this project. All other trademarks belong to their respective owners.

## License

[MIT](LICENSE)
