termfleet
v0.2.0
Published
An ergonomic control center for AI terminal sessions (Claude Code, Codex, Gemini) across tools, projects, and hosts.
Maintainers
Readme
termfleet
An ergonomic control center for the AI coding sessions you run — Claude Code, Codex, and Gemini — across tools, projects, and machines.
termfleet is an orchestration layer over the agents already running in your terminals: one place to see which sessions need you and act on any of them by name, with a visual fleet canvas when you want it. It attaches to the real terminals on your host(s) — iTerm2, WezTerm, or a managed virtual-tmux server — and gives every session a durable identity you can list, triage, read, reply to, wait on, continue, and close, from a CLI built for an agent to drive, an SDK, or a browser console. It is not another "spawn N isolated agents" runner; it controls the agents you already have.
Quickstart (local, no Docker/registry/account)
Install the CLI (Node 20+):
npm install -g termfleetThen start a console + one local provider and launch a session:
# a console (UI + aggregator) and one local provider:
termfleet console serve --name dev --port 7373 &
termfleet provider serve --kind virtual-tmux --prefix dev --count 1 --port 7402 &
# the CLI auto-discovers the local provider — no --url needed:
termfleet claude new --prompt "hello" # launch a Claude session
termfleet sessions recent --live --sort attention # which sessions need you?Open http://127.0.0.1:7373 for the visual console. That's the whole local
experience. (termfleet --help lists every command; termfleet --version
prints the version.)
Working from a clone (contributing)? You don't need a global install — run the CLI straight from source with
npx tsx src/cli.ts …in place oftermfleet …:git clone https://github.com/volter-ai/termfleet.git cd termfleet && npm install && npm run build # build powers the browser console UI npx tsx src/cli.ts console serve --name dev --port 7373 &
Concepts
- Console — the UI + aggregator: serves the board, proxies providers, merges the fleet across hosts. One per user.
- Provider — a process that owns terminal windows on a host (iTerm2 mirror, WezTerm, or virtual-tmux). The console talks to many.
- Session — the unit you actually work with: a Claude/Codex/Gemini
conversation, addressed by
agentSessionId— top-levelsessions,send, andclose, plus per-agentclaude|codex|gemini get|wait|continue— never by hunting for a terminal id.
What it's good at — and what it isn't
The edge is the combination: cross-host + cross-tool (Claude/Codex/Gemini) + a visual canvas + optional team/org sharing, all through one session-first model. Single-host triage of Claude sessions alone is also free in Claude Code's Agent View; termfleet's value is spanning hosts, tools, and people.
Per-tool capability today:
| tool | discover / read | launch (new) | wait / get | continue |
|---|---|---|---|---|
| Claude | ✅ | ✅ | ✅ | ✅ |
| Codex | ✅ | ✅ (no caller-bound session id) | ✅ | ✖ (no --resume) |
| Gemini | ✅ | ✅ | ✅ | ✖ (resumes by index, not id) |
Project status: pre-1.0, moves fast, primarily one maintainer, heavily
AI-assisted. The macOS native path (iTerm2 via osascript) is most-tested; the
portable path is virtual-tmux/Docker; WezTerm-on-WSL is unvalidated. Providers
run real processes and serve host files — treat "provider reachable" as "full
host access granted." Default is loopback-only; read SECURITY.md before
exposing anything.
Requirements
- Minimum (local): Node.js 20+ and
tmux(brew install tmux). - To launch agents: the agent CLI you'll use —
claude,codex, orgemini— installed on PATH and logged in. The Quickstart's firstclaude newfails after ~45s against a missing or not-logged-in CLI, so install and sign in first (e.g. runclaude, then/login). - Optional, per feature: Docker (Docker workers) · macOS + iTerm2 (iTerm provider) · a deployed registry (team/remote — see below).
Team & remote (opt-in): Docker Worker, Registry, Tunnel
This is the recommended path for a new user who wants terminals in Docker, shared team discovery, and a dry run before handing the setup back to a human.
The registry is opt-in. With
TERMFLEET_REGISTRY_URLunset, termfleet is local-only and makes no outbound requests — that is the default experience (see the local Quickstart above). The steps in this section apply only if you have deployed and configured your own registry.Deploy + seed it first (the worker has no sign-up flow — accounts come from seeded users set at deploy time). From
packages/registry-worker:npx wrangler secret put TERMFLEET_SEEDED_PASSWORD --env="" # required; no default npx wrangler secret put TERMFLEET_SEEDED_USERS --env="" # JSON array of your team's accounts npx wrangler deploy --env=""See
packages/registry-worker/README.mdfor theTERMFLEET_SEEDED_USERSshape. Then pointTERMFLEET_REGISTRY_URLat the deployed worker's/providersendpoint, and sign in (step 3) as one of those seeded emails:TERMFLEET_REGISTRY_URL=https://your-registry.example.com/providers
The Docker worker is still your machine's responsibility. The registry stores auth, organization membership, shared provider records, canvas layout/comments, and short-lived provider access tokens. It does not run Docker or tmux.
1. Install
git clone https://github.com/volter-ai/termfleet.git
cd termfleet
npm install
npm run buildMake sure Docker is running before starting a Docker worker. If you use Colima, start it first and export the Docker socket if your shell needs it:
colima start
export DOCKER_HOST="unix://$HOME/.colima/default/docker.sock"2. Start A Local Console
Run the console locally, pointed at your registry via TERMFLEET_REGISTRY_URL
(the registry is opt-in; without this env var the console is local-only).
TERMFLEET_REGISTRY_URL="https://your-registry.example.com/providers" \
npx tsx src/cli.ts console serve \
--name local-console \
--host 127.0.0.1 \
--port 7373 \
--local-adapters noneOpen:
http://127.0.0.1:7373/?session=local-console#view=providers3. Sign In To The Registry
Use the browser sign-in flow or the CLI. The CLI stores its session in
~/.termfleet/registry-auth.json and uses the same registry as the console.
npx tsx src/cli.ts registry login \
--identifier [email protected] \
--password-stdinConfirm the active user and organization:
npx tsx src/cli.ts registry session
npx tsx src/cli.ts registry list4. Start Your Docker Worker
From the console UI, click Start or register provider, then Start Docker
worker.
The same action is available from the CLI:
npx tsx src/cli.ts registry start-docker-worker \
--label "My Docker Worker"This builds/starts the Compose worker and registers
http://127.0.0.1:7374 with your local console. The worker runs as the
unprivileged termfleet user, and created sessions start in /workspace.
The raw Docker provider URL is an API endpoint for the console and CLI. Do not
expect http://127.0.0.1:7374/ to be the full app. Use the console at
http://127.0.0.1:7373/ to see and control it.
5. Dry Run Locally
Use the browser first. In Playwright or a normal browser:
- Open
http://127.0.0.1:7373/?session=local-console#view=providers. - Confirm
My Docker Workerappears as connected. - Confirm the canvas shows a
Virtual tmux Space. - Create or open a terminal, then type:
whoami && pwd && touch /workspace/termfleet-dryrun-ok && ls /workspace/termfleet-dryrun-okExpected output includes:
termfleet
/workspace
/workspace/termfleet-dryrun-okThen verify the CLI can reach the same worker:
npx tsx src/cli.ts providers health --url http://127.0.0.1:7374
npx tsx src/cli.ts providers snapshot --url http://127.0.0.1:7374
npx tsx src/cli.ts list --url http://127.0.0.1:7374For raw shell repair-only diagnostics, create a temporary shell and capture it:
npx tsx src/cli.ts new --repair \
--url http://127.0.0.1:7374 \
--break-glass \
--cwd /workspace \
--command "whoami && pwd"Use capture on the returned terminalId value.
6. Log In Inside The Docker Worker
The Docker worker has isolated volumes for /home/termfleet and /workspace.
Credentials for Claude Code, Codex, GitHub CLI, SSH, and workspace files live
inside those Docker volumes, not on the host unless you explicitly mount them.
Use the console terminal for interactive logins:
claude /login
codex
gh auth loginOr open a repair shell from the CLI when the UI is not enough:
npx tsx src/cli.ts new --repair \
--url http://127.0.0.1:7374 \
--break-glass \
--cwd /workspace \
--command "gh auth status"After login, prefer agent windows for normal work:
npx tsx src/cli.ts claude new \
--url http://127.0.0.1:7374 \
--name claude-worker \
--cwd /workspace \
--prompt "Inspect the workspace and report setup status."
npx tsx src/cli.ts codex new \
--url http://127.0.0.1:7374 \
--name codex-worker \
--cwd /workspace \
--prompt "Inspect the workspace and report setup status."7. Make Your Docker Worker Available To The Team
Public/shared providers must be authenticated. Start a provider that requires a registry-issued provider token, expose it with an HTTPS tunnel, then register the tunnel URL as a shared provider.
Critical: set
TERMFLEET_PROVIDER_PUBLIC_URLto the provider's public tunnel origin (the same URL you register below). That is what switches the provider into shared mode — accepting only org-scoped provider tokens minted for the owning org. Without it, a public provider falls back to bare-session auth and any signed-in registry user of any org can reach it. You must therefore decide your tunnel origin first (reserve the subdomain).
Start an auth-required Docker provider on a local port (replace the public URL with your reserved tunnel origin):
docker run --rm \
--name termfleet-team-worker \
-p 127.0.0.1:7374:7373 \
-e TERMFLEET_AUTH_REGISTRY_URL=https://your-registry.example.com \
-e TERMFLEET_ALLOWED_ORIGINS=https://your-registry.example.com \
-e TERMFLEET_PROVIDER_PUBLIC_URL=https://YOUR-TUNNEL-SUBDOMAIN.your-tunnel-host.example.com \
termfleet:local \
node /app/dist/cli.js provider serve \
--kind virtual-tmux \
--host 0.0.0.0 \
--port 7373 \
--prefix termfleet-docker \
--count 0 \
--cwd /workspace \
--tunnelExpose 127.0.0.1:7374 through any HTTPS tunnel. For most users a standalone
tunnel like ngrok or cloudflared is the simplest option:
ngrok http 7374If you run your own tunnel relay instead, point TERMFLEET_LOCAL_TUNNEL_BIN at
your own tunnel client binary and set TERMFLEET_TUNNEL_SERVER_URL to the relay
URL (the --tunnel flag then spawns that client for you). Whatever client you
use, it must forward to the provider port and not add its own auth gate here; the
Termfleet provider
is already enforcing registry-issued provider tokens because it was started
with --tunnel.
Register the HTTPS tunnel URL in the active registry organization:
npx tsx src/cli.ts registry register-shared \
--url https://YOUR-TUNNEL-SUBDOMAIN.your-tunnel-host.example.com \
--label "Your Name Docker Worker" \
--requires-authTeam members in the same active organization will see it under Shared Providers in the cloud console and any local console using the same registry.
8. Use It Remotely
Open the cloud console from another machine:
https://your-registry.example.com#view=providersSign in, confirm the shared provider is connected, then use the canvas normally.
The CLI can also use shared providers by name after login:
npx tsx src/cli.ts registry login --identifier [email protected] --password-stdin
npx tsx src/cli.ts providers snapshot --provider "Your Name Docker Worker"
npx tsx src/cli.ts claude new \
--provider "Your Name Docker Worker" \
--cwd /workspace \
--prompt "Confirm this remote Docker worker is usable."Remove a shared provider when the tunnel or worker is no longer meant to be available:
npx tsx src/cli.ts registry unregister-shared \
--url https://YOUR-TUNNEL-SUBDOMAIN.your-tunnel-host.example.comFeature Guide
Termfleet exposes the same provider features through the browser console, CLI, and SDK.
- Providers: start local virtual tmux, Docker, iTerm, or WezTerm providers; register local URLs; register shared team URLs; remove stale providers.
- Canvas: pan, zoom, fit view, inspect provider groups, and arrange terminal windows.
- Terminal windows: create agent windows, move/resize windows, close windows, capture output, and send input.
- Agents: launch Claude Code, Codex, or Gemini in provider-owned terminals and
read structured session state with
claude get,codex get,gemini get, andwait. - Files: read, write, stat, and copy files through provider filesystems.
- Folders/files: open folder and file views on the provider canvas.
- Iframes: open an HTTP app or preview URL on the provider canvas.
- Multiplayer: registry-backed canvas layout, comments, presence, cursors, viewport, and selection sync are scoped to the active organization.
- Security: shared public providers should use provider auth (
--tunnelplus shared registration with--requires-auth); raw terminal shells are repair-only with--break-glass.
Provider Model
Console / CLI
-> ProviderClient SDK
-> Provider Instance URL
-> ProviderEngine
-> ItermDriver, VirtualTmuxDriver, or WeztermDriver
-> iTerm, tmux virtual display, or WezTerm terminalsThe console server owns the browser UI, registry/auth, and local provider
materialization. Provider servers own terminal/window/display state only. The CLI
uses the same ProviderClient SDK as the console for provider operations, so it
does not need the console server for normal provider control.
Console + providers, in depth
A deeper look at the local setup the Quickstart
covers: run the console on your computer, start one or more provider servers, then
drive them from the console or CLI. No registry and no tunnel are required — leave
TERMFLEET_REGISTRY_URL unset and termfleet makes no outbound requests. Locally the
console auto-discovers running providers (and the CLI auto-resolves a target),
so explicit registration is optional — the SDK/CLI registration shown below is for
when you want to register a provider explicitly or by a remote URL.
The numbered walkthrough below runs from a clone (
npx tsx src/cli.ts …); with a global install, substitutetermfleet …(nonpm install/npm run build).
For the fastest path into a ready control session, use the idempotent chat command:
termfleet chatchat ensures the local console is running, ensures the configured local
providers exist, optionally opens or focuses the console, then reuses or starts
the configured Claude/Codex chat inside a Termfleet provider. Flags override
saved settings for one run; add --save to make those choices the new default:
termfleet chat --save --agent claude --agent-provider iterm --providers docker-worker,iterm --open-console
termfleet chat --agent none --providers virtual-tmux --no-open-console- Start the console:
npm install
npm run build
npx tsx src/cli.ts console serve --name local-console --host 127.0.0.1 --port 7373- Start a provider server:
npx tsx src/cli.ts provider serve --kind virtual-tmux --prefix termfleet-virtual --count 2 --host 127.0.0.1 --port 7376- (Optional — local providers are auto-discovered.) To register a provider with the console explicitly (e.g. a remote URL), use the SDK:
import { ProviderClient, providerRefFromUrl } from "termfleet";
const consoleClient = new ProviderClient(providerRefFromUrl("http://127.0.0.1:7373", "Local console"));
const provider = await consoleClient.registerLocalProvider({
baseUrl: "http://127.0.0.1:7376",
label: "Virtual tmux"
});
console.log({ provider });
consoleClient.disconnect();The console can also materialize local providers itself through
startLocalProvider. Docker workers use the same provider protocol: the
container runs a virtual tmux provider and the console registers that provider
URL.
- Expose a provider with a tunnel before registering a shared URL:
ngrok http 7376Use any tunnel or LAN URL that other users can reach. Keep both the provider and
tunnel running while the provider is shared. Providers are unauthenticated by
default; mark a provider as auth-required when its own --tunnel mode exposes
it publicly.
- The CLI exposes the same SDK path as a convenience wrapper:
npx tsx src/cli.ts registry login --identifier [email protected] --password-stdin
npx tsx src/cli.ts registry start-docker-worker
npx tsx src/cli.ts registry register-shared \
--url https://your-provider-tunnel.example.com \
--alias your-worker \
--label "Your Name Provider" \
--requires-authThe CLI stores its registry session token in ~/.termfleet/registry-auth.json.
The console also uses this token for registry reads when the browser has not
supplied its own signed-in session.
- Open the console:
open http://127.0.0.1:7373/The console starts local iTerm and WezTerm adapters by default, so they appear under Console-Managed Providers when their backing terminal apps are available. Virtual tmux, Docker worker, and shared providers appear in their own sidebar groups after they are started or registered.
WezTerm on WSL/Windows (experimental)
On WSL/Windows the WezTerm provider is the native-terminal candidate. It's implemented and smoke-tested on macOS but not yet validated on a real WSL machine — keep the virtual-tmux provider as the WSL fallback until it is.
To try it: install WezTerm on Windows, use a WSL distro with node/npm/tmux
and the termfleet checkout, and confirm wezterm.exe and powershell.exe are
visible from WSL (it drives the terminal via wezterm.exe and Win32 window
positioning via powershell.exe). Then:
npx tsx src/cli.ts provider serve --kind wezterm --prefix wsl-wezterm --count 1 --host 0.0.0.0 --port 7377http://localhost:7377/healthz should return "provider":"wezterm". If
enumeration or window positioning misbehaves from WSL, that's the unvalidated
path — please file an issue (see CONTRIBUTING).
Start A Provider
A provider owns terminal windows on a host. Run one on its own port (distinct from the console's):
termfleet provider serve --kind virtual-tmux --prefix agent-a --count 2 --host 127.0.0.1 --port 7402Open the provider canvas, or control it entirely from the CLI:
open http://127.0.0.1:7402/
termfleet providers health --url http://127.0.0.1:7402
termfleet providers snapshot --url http://127.0.0.1:7402Run The Console
Run a console when you want to see the Termfleet app in the browser and load providers from the local pool and shared registry.
termfleet console serve --name local-console --host 127.0.0.1 --port 7373Open the app:
open http://127.0.0.1:7373/The console automatically loads providers from .termfleet-registry.json and,
when TERMFLEET_REGISTRY_URL is set, from the shared registry (it is opt-in;
without that env var the console stays local-only). The Providers sidebar can
register any reachable URL
that implements the provider API. Provider names shown in the sidebar come from
the registry's label field.
The sidebar can also start additional local providers for you. Start virtual
tmux and Start Docker worker add those provider URLs to the console runtime
provider list. They do not need to be written to .termfleet-registry.json
because the console process owns those materialized providers directly. The local
iTerm provider is on by default.
Start The Docker Worker
Use the Docker worker when you want Termfleet terminals to run inside a Docker container. The container exposes a normal virtual tmux provider endpoint to the console or CLI.
- Clone and install the project:
git clone https://github.com/volter-ai/termfleet.git
cd termfleet
npm install- Start the Docker worker with Compose:
docker compose up --buildCompose publishes the provider at http://127.0.0.1:7374. The provider itself
listens on port 7373 inside the container.
- Open the provider canvas:
open http://127.0.0.1:7374/The Docker worker's virtual tmux provider should render its terminal windows in the browser.
To see the worker provider from a separate console, keep this Docker process running and register its reachable URL from the console's Providers sidebar.
To run without Compose:
npm run docker:build
docker run --rm \
-p 127.0.0.1:7374:7373 \
termfleet:localBy default the worker provider starts with no panels. Panels are created
explicitly with claude new, codex new, folder open, or repair commands.
To seed initial terminal panels for debugging or to change the prefix, run the
provider CLI directly in the built image:
docker run --rm \
-p 127.0.0.1:7374:7373 \
termfleet:local \
node dist/cli.js provider serve --kind virtual-tmux \
--host 0.0.0.0 \
--port 7373 \
--prefix my-docker-worker \
--count 2Bundled Agent CLIs
The Docker image ships with agent tooling so the terminals inside the worker are
ready to drive: the GitHub CLI (gh), Claude Code
(@anthropic-ai/claude-code), and Codex (@openai/codex). The Gemini CLI is
not bundled — install it in the worker (or a derived image) if you want
gemini new there. The binaries are installed in the image; credentials live in
the container filesystem or mounted home directory supplied when the worker is
created.
The termfleet:local image does not include or mount local machine agent credentials.
Each Docker worker gets its own Compose-project-scoped Docker volumes for
/home/termfleet and /workspace, so Claude, Codex, GitHub CLI, SSH config,
and workspace files are isolated per worker. Run claude /login, codex, or
gh auth login inside the worker that should own those credentials.
Create Another Docker Worker
The Start Docker worker button starts the default worker at
http://127.0.0.1:7374. To run another worker provider at a different local
port, build the image and run a second container:
npm run docker:build
docker run --rm \
-p 127.0.0.1:7405:7373 \
termfleet:local \
node dist/cli.js provider serve --kind virtual-tmux \
--host 0.0.0.0 \
--port 7373 \
--prefix termfleet-docker-7405 \
--count 2This creates another Docker worker provider at http://127.0.0.1:7405. To make it
available through the app, create a tunnel for port 7405 and register that
tunnel URL.
Tunnel And Register A Provider
To share a provider with the team, expose its local port through a tunnel and register the tunnel URL under Shared Providers.
For the default Docker worker provider:
ngrok http 7374For an additional worker provider running on port 7405:
ngrok http 7405Copy the HTTPS forwarding URL from the tunnel output, then open the console
at http://127.0.0.1:7373/ and register it in the Providers sidebar:
- Section: Shared Providers
- Name: a useful team-visible name, such as
Alice Worker - URL: the tunnel HTTPS URL
- Click
Register
Registered tunnel URLs are written to the shared registry. The tunnel process and provider process both need to stay running while the team uses that provider.
Add It To The Registered Pool
A Termfleet console loads runtime local providers, console-local saved
providers, and shared remote providers. Runtime local providers are the iTerm
and WezTerm adapters started by the console process; they appear in the provider
list while that console process is running and do not need to be saved to
.termfleet-registry.json. Manually added local providers can still be stored
in .termfleet-registry.json. Remote providers are read from and written to the
shared registry URL you configure via TERMFLEET_REGISTRY_URL:
https://your-registry.example.com/providersThe registry is opt-in: with TERMFLEET_REGISTRY_URL unset, termfleet is
local-only and never contacts a remote registry. Set it to point every user at
the same persistent remote pool — deploy your own registry worker from
packages/registry-worker. The registry must return either an array of provider
records or an object with a providers array. Each provider needs a reachable
baseUrl; label is optional but recommended.
A deployed registry worker is the normal team registry. Only run a local registry when developing the registry service itself:
npx tsx src/cli.ts serve-registry \
--host 127.0.0.1 \
--port 7401 \
--file .termfleet-remote-registry.jsonIf you deliberately expose that local development registry, point any HTTPS
tunnel at port 7401 with its own auth gate disabled so the registry service can
handle /auth/* and /providers authentication itself:
ngrok http 7401(ngrok and cloudflared are the simplest standalone tunnels. To use a
self-hosted relay instead, set TERMFLEET_LOCAL_TUNNEL_BIN to your own tunnel
client binary and TERMFLEET_TUNNEL_SERVER_URL to the relay URL.)
Keep both the registry server and the tunnel process running. The remote registry file should contain only remote or tunnel URLs, not localhost providers.
Open the console at http://127.0.0.1:7373/ and use the Providers sidebar.
The sidebar groups providers by role:
- Console-Managed Providers: console-started iTerm and WezTerm adapters.
- Virtual Servers: local virtual tmux providers.
- Docker Workers: Docker-hosted virtual tmux providers.
- Shared Providers: remote provider URLs from the shared registry.
You only need a reachable provider URL; the person running the provider does not have to be the same person who registers it.
- Name:
Worker Provideror any display name you want. - URL: a tunnel URL for your provider or someone else's provider.
- Requires auth: enable this only when the provider URL points at a
termfleet provider serve --tunnelinstance. - Click
Registerin the Shared Providers form.
The local iTerm and WezTerm start buttons materialize default loopback URLs into
the console runtime provider list only. The register form writes tunnel URLs to
the shared registry. Refreshing the console will load persisted local virtual
tmux, Docker worker, and shared provider URLs from /api/registry/providers.
Termfleet uses one configured remote registry service with organization-scoped provider records. Switching the active organization changes which shared providers you see and manage. Removing a local provider deletes only this console's local registry entry. Unregistering a shared provider deletes that provider from the active organization's shared registry so other org members stop seeing it.
Example shared registry response:
{
"providers": [
{
"baseUrl": "http://127.0.0.1:7374",
"label": "Docker Worker"
}
]
}Use a URL that the intended console or CLI client can reach. A local console can register
http://127.0.0.1:7374 into its local pool. To share the same provider with
other users, register a LAN hostname or tunnel URL so their machines can reach
it.
To use a different registry, start the console with TERMFLEET_REGISTRY_URL
pointed at that registry endpoint:
TERMFLEET_REGISTRY_URL="https://registry.example.com/providers" \
npx tsx src/cli.ts console serve --name local-console --port 7373Open the console and it will automatically connect to every local provider plus every provider returned by the remote registry:
open http://127.0.0.1:7373/Use the Providers sidebar to verify what the console sees.
Small Public Deployment
For a small public deployment, expose the console through tunnel mode instead of publishing the raw Termfleet port:
TERMFLEET_CONSOLE_TOKEN="a-strong-random-secret" \
TERMFLEET_ALLOWED_ORIGINS="https://your-console-tunnel.example.com" \
TERMFLEET_CONSOLE_ALLOWED_ORGS="org_your_org" \
npx tsx src/cli.ts console serve --name public-console --port 7373 --tunnel --tunnel-id your-stable-console-idA tunnel always has an auth backend — it never serves the fleet unauthenticated. The backend resolves by precedence:
TERMFLEET_CONSOLE_TOKEN(a strong operator secret) → a configured registry (user accounts) → else the console auto-provisions a 6-digit access passcode and prints it at startup (chatprints it too). The passcode is a rate-limited login factor exchanged at/console-login— fine for personal use, but setTERMFLEET_CONSOLE_TOKEN(or use a registry) for anything shared or long-lived, where a strong secret beats a 6-digit code. Loopback access is always exempt; only remote/tunnel peers are gated. If a passcode leaks, runtermfleet console rotate-passcodeand restart the console.
Tunnel client setup
The simplest way to publish the console is a standalone tunnel — run ngrok or
cloudflared yourself against the console port and open the HTTPS URL it prints:
ngrok http 7374The console still gates remote peers as long as it was started with --tunnel
(or otherwise bound to a non-loopback interface).
If you instead want the console to spawn and supervise the tunnel for you (so
--tunnel brings it up automatically), point TERMFLEET_LOCAL_TUNNEL_BIN at
your own tunnel client binary and set TERMFLEET_TUNNEL_SERVER_URL to your relay
URL. Termfleet ships no tunnel client — this is the seam for plugging in your
own.
If your client isn't a directly executable file — for example a script entry the
shell can't run as-is — wrap it in a small launcher and point
TERMFLEET_LOCAL_TUNNEL_BIN at the launcher:
mkdir -p ~/.termfleet/bin
cat > ~/.termfleet/bin/my-tunnel <<'SH'
#!/bin/sh
exec /path/to/your/tunnel-client "$@"
SH
chmod +x ~/.termfleet/bin/my-tunnel
export TERMFLEET_LOCAL_TUNNEL_BIN="$HOME/.termfleet/bin/my-tunnel"
export TERMFLEET_TUNNEL_SERVER_URL="https://your-relay.example.com"These two values (plus TERMFLEET_REGISTRY_URL) are persisted: run
termfleet chat --save once from a shell that has them set, and every later
console serve/chat recovers them from ~/.termfleet/settings.json — so a
console launched from a different shell still finds its relay and client.
Keep provider and Docker ports bound to 127.0.0.1. If you run the registry on
a public interface, set TERMFLEET_SEEDED_PASSWORD and
TERMFLEET_REGISTRY_ALLOWED_ORIGINS explicitly; the registry refuses public
startup with the default seeded password.
AI Agent Workflow
termfleet is built to be driven by an AI orchestrator, and the model is
session-first: you discover, read, and act on sessions by agentSessionId —
you almost never name a terminal or window. (For browser-terminal validation,
run the agent CLIs in their interactive TUI mode; don't use non-interactive
shortcuts like claude -p / codex exec, which prove a different execution path
than a user driving the terminal through termfleet.)
1. Triage — which sessions need you?
termfleet sessions recent --live --sort attention # the attention / triage view
termfleet sessions search --query "deploy" # find a session by contentOne list across every provider, sorted by which sessions are waiting on you.
2. Read a session — by id, with a since-cursor for deltas:
termfleet claude get --agent-session-id claude:<uuid> --cwd /workspace
termfleet claude get --agent-session-id claude:<uuid> --since 12 # only items after #12Or block until the session finishes its current turn — the primitive you build loops on:
termfleet claude wait --agent-session-id claude:<uuid> --after-count 5 --timeout-ms 600000--after-count N waits for a turn after the Nth message, so a wait issued
right after a send returns the new reply, not the one already there.
3. Act on a session — reply or close, keyed by id:
termfleet send --agent-session-id claude:<uuid> --text "run the tests" --enter
termfleet close --agent-session-id claude:<uuid>termfleet resolves session → window → terminal internally; you never supply a
terminalId.
4. Launch or resume:
termfleet claude new --cwd /workspace --prompt "Inspect this repo and report before editing."
termfleet codex new --cwd /workspace --name todo-codex
termfleet claude continue --agent-session-id claude:<uuid> # resume an ended Claude sessionclaude new starts Claude Code with an explicit --session-id and prints it as
the agentSessionId (pass --agent-session-id ID to choose it). codex new
doesn't accept a launch-bound id yet, and continue is Claude-only (Codex
has no --resume; Gemini resumes by index). The CLI is biased toward reuse —
new refuses to add a panel when a reusable one exists and prints its capture so
you can decide where to send next. --cwd is passed to the provider at creation
time and must exist in the new terminal environment (for Docker workers,
/workspace is the container filesystem).
For multiple machines, target a provider with --provider <alias> / --url <url>
(localhost, a remote host, or a tunnel URL); with no target the CLI auto-resolves
a local one. Auth stays off unless the provider record sets requiresAuth.
Break-glass: terminals and windows (plumbing)
Below the session layer is the raw transport — terminals and board windows. You
should rarely touch it; these verbs name a terminalId / window id directly and
exist for diagnosis and repair:
termfleet list --url <url> # board windows
termfleet capture --url <url> --terminal-id <id> --lines 40 # raw pane capture
termfleet send --url <url> --terminal-id <id> --break-glass --text "pwd" --enter # --break-glass required for a non-agent shell
termfleet terminals move --url <url> --id 1 --left 80 --top 80 --width 900 --height 520
termfleet close --url <url> --id 1 # by window id
# raw shell creation — repair-only, when Claude/Codex can't start or the provider needs diagnosis:
termfleet new --repair --url <url> --break-glass --cwd /workspace --command "gh auth status"MCP server
The same session-first control verbs the CLI exposes are also available over MCP,
so any MCP-capable agent (Claude Code, Claude Desktop, or your own host) can drive
the fleet — list sessions, read a transcript, reply, launch, continue, and close —
without shelling out. The termfleet-mcp bin is a stdio MCP server and a thin
client of the same provider resolution the CLI uses (TERMFLEET_PROVIDER_URL →
termfleet use context → live local discovery), so it adds no new behavior, just a
second front door.
Tools: list_sessions, get_session (supports since for catch-up and cwd to
resolve some transcripts), send_to_session, new_session (accepts cwd / name
/ prompt), continue_session, close_session. Sessions are addressed by their
prefixed agentSessionId (e.g. claude:<uuid>); window/terminal resolution happens
internally.
Register it with an MCP host, e.g. in Claude Code's config:
{
"mcpServers": {
"termfleet": { "command": "termfleet-mcp" }
}
}termfleet-mcp is on PATH after a global install (npm install -g termfleet);
otherwise use { "command": "npx", "args": ["-y", "termfleet-mcp"] }, or from a
clone { "command": "npx", "args": ["tsx", "<repo>/src/mcp/server.ts"] }.
Point it at a specific provider by setting TERMFLEET_PROVIDER_URL in the server's
env; otherwise it auto-discovers a local provider on first use.
Library: reading agent transcripts (@termfleet/core)
The transcript layer is usable on its own, beyond the console. Install it standalone — a small Node-only library that reads and normalizes Claude Code / Codex / Gemini sessions off disk into one model:
npm install @termfleet/coreimport { readLocalAgentSession } from "@termfleet/core/agent-session.js";
import { parseAgentSessionId } from "@termfleet/core/agent-session-id.js"; // claude:/codex:/gemini: grammar
const session = readLocalAgentSession({ cwd: process.cwd(), sessionId: "claude:<uuid>" });
console.log(session.messages.length, session.lastAssistantText);See packages/core/README.md for the full surface —
per-agent readers, the id grammar, the streaming tail, and the provider SDK
(ProviderClient, for driving a provider). If you already depend on
termfleet, the same readers are re-exported from termfleet/sessions as a
convenience.
CLI Commands
Sessions (the primary surface — tier 1)
Work by agentSessionId; termfleet resolves window→terminal internally. This is
what an orchestrator should use — windows and terminals (below) are plumbing.
sessions recent [--live] [--sort recent|attention] [--fleet] [--limit N] [--cursor C] [--console-url URL]: list sessions across providers.--sort attentionis the triage view;--livestreams updates.sessions search --query TEXT [--limit N] [--console-url URL]: find sessions by content.sessions watch [--interval-ms N] [--console-url URL]: poll the session list on an interval.<claude|codex|gemini> get --agent-session-id ID [--cwd PATH] [--since N]: read a session's normalized transcript;--since Nreturns only items after #N.<claude|codex|gemini> wait --agent-session-id ID [--after-count N] [--timeout-ms N] [--poll-ms N]: block until the session's current turn ends.--after-count Nwaits for a turn after the Nth message (use it right aftersend).claude continue --agent-session-id ID [--cwd PATH] [--prompt TEXT]: resume an ended Claude session (claude --resume). Claude only.<claude|codex> list: list that agent's sessions on the provider.<claude|codex> kill (--id ID | --name NAME): stop a running agent session.send --agent-session-id ID --text TEXT [--enter]: reply to a session.close --agent-session-id ID: close a session.
send / close also accept --terminal-id / --id as break-glass — see the
window/terminal plumbing below.
Provider resolution
Every command that talks to a provider accepts --url URL or --provider TARGET,
but both are optional. When neither is given, termfleet resolves a provider in
this order (the same model Docker/kubectl/tmux use for local defaults):
--url/--provider— explicit, wins over everything.$TERMFLEET_PROVIDER_URL— the env var agents and CI use to inject a target.- Current context — a default you pin with
termfleet use <url|name>. - Live local auto-discovery —
provider serveself-advertises to~/.termfleet/providers/, and the CLI picks the single one that is actually serving (verified via/healthz, so a stale record never wins). - Otherwise a structured error (
no_providerorambiguous_provider) with aremedyfield telling you how to start or pick one.
This means termfleet claude list, termfleet claude new …, etc. just work when
one provider is running locally — no URL required. Helpers:
termfleet resolve [--url URL | --provider TARGET]— print which provider would be chosen and why (thesourcefield), read-only. Use this instead of guessing when a command can't find a provider.termfleet use <url|name>— pin a default provider;use --show/use --clearto inspect or unset it. Agents should prefer$TERMFLEET_PROVIDER_URL(which outranks the pinned default) so they never depend on shared mutable state.termfleet doctor— now also reports live local providers and what the default would resolve to.
Resolution failures are emitted as JSON on stderr with a stable error code and a
remedy, and always exit nonzero — so an agent can branch on the cause rather
than parse prose.
- SDK: import
ProviderClientandproviderRefFromUrlfromtermfleet. Console setup methods includesignInToRegistry,registrySession,switchRegistryOrganization,registryProviders,startDockerWorker,startLocalProvider,registerLocalProvider,unregisterLocalProvider,registerSharedProvider,unregisterSharedProvider, andsignOutOfRegistry. providers health --url URL: prints provider health.providers snapshot --url URL: prints display bounds and terminal windows.providers lifecycle --url URL: prints observed process/session lifecycle.- Provider targets passed with
--providercan be a saved alias, provider id, exact label, or URL. Save aliases during registration with--aliasor--aliases, or update an existing provider withproviders alias set. registry login [--console-url URL] --identifier EMAIL (--password PASSWORD | --password-stdin): signs in to the shared registry through the console SDK.registry logout [--console-url URL]: clears the saved CLI registry session.registry session [--console-url URL]: prints the current registry account and active organization.registry list [--console-url URL]: lists providers visible to the local console, including shared providers when signed in.registry start-docker-worker [--console-url URL] [--url URL] [--label LABEL] [--alias ALIAS] [--aliases A,B] [--terminal-theme-profile PROFILE]: asks the console to start the default Docker worker provider.registry register-local [--console-url URL] --url URL [--label LABEL] [--alias ALIAS] [--aliases A,B] [--requires-auth]: stores a local/loopback provider in the console-local registry.registry register-shared [--console-url URL] --url URL [--label LABEL] [--alias ALIAS] [--aliases A,B] [--requires-auth]: registers a reachable provider URL in the active organization. Use--requires-authfor public or tunneled providers.registry unregister-shared [--console-url URL] (--provider TARGET | --url URL): unregisters a shared provider URL from the active registry organization.providers alias list: lists saved provider aliases.providers alias set (--provider TARGET | --url URL) --alias ALIAS: saves an alias for an existing provider.providers alias unset (--provider TARGET | --url URL) --alias ALIAS: removes an alias from an existing provider.list --url URL: lists provider-owned terminals.capture --url URL --terminal-id TERMINAL_ID [--lines N]: captures terminal output.send --url URL --terminal-id TERMINAL_ID --text TEXT [--enter]: sends input to a provider-owned agent terminal.close --url URL --id ID: closes a provider-owned terminal window.claude new --url URL [--name NAME] [--cwd PATH] [--prompt TEXT]: creates an agent terminal and startsclaude --dangerously-skip-permissions.codex new --url URL [--name NAME] [--cwd PATH] [--prompt TEXT]: creates an agent terminal and startscodex --dangerously-bypass-approvals-and-sandbox --dangerously-bypass-hook-trust.terminals move --url URL --id ID --left N --top N --width N --height N: moves a provider-owned terminal window.terminals resize-display --url URL --width N --height N: resizes a virtual provider display.new --repair --url URL --break-glass [--name NAME] [--cwd PATH] [--command COMMAND]: creates a raw provider-owned shell for repair use only. Preferclaudeorcodexfor normal work.console serve --name NAME [--port PORT] [--host HOST] [--local-adapters iterm[:PORT],wezterm[:PORT]|none] [--no-auto-local-adapters] [--tunnel] [--tunnel-id ID]: starts the browser console server.--tunnelpublishes it at a stable public URL (see "Small Public Deployment" andSECURITY.md);--local-adapterspicks the always-on iTerm/WezTerm mirrors.console rotate-passcode [--name NAME]: rotates the 6-digit tunnel access passcode (new code, same signing secret — existing sessions survive); takes effect on the next console start.provider serve --kind iterm --name NAME [--port PORT] [--host HOST]: starts an iTerm-backed provider server.provider serve --kind virtual-tmux [--prefix PREFIX] [--count N] [--port PORT] [--host HOST]: starts a virtual tmux provider server.--countdefaults to0.provider serve --kind wezterm [--prefix PREFIX] [--count N] [--port PORT] [--host HOST]: starts an experimental WezTerm provider server.--countdefaults to0.registry start-docker-worker [--console-url URL]: asks the console to start the Docker worker and register its provider URL.
Driving a session from your terminal
The console mirrors every session in the browser, but you don't have to leave the terminal to type into one. Two ideas, kept lexically separate so they never blur into each other — create (boot a session) vs connect (attach to a live one):
attach [SESSION | --agent-session-id ID]: connect the calling terminal directly to a live session — the raw PTY, the same stream the browser shows. With no argument it attaches to the single live session; otherwise pass anagentSessionId(or a prefix / window name). Detach with Ctrl-b d — the session keeps running (it's durable on disk and reaped independently); only an explicit exit ends it.attachnever boots anything.chat --attach: the normalchatbring-up (console + fleet + a launched agent), but instead of printing JSON it drops you straight into the new session. Barechatstill reports JSON and exits, so scripts are unaffected.control(aliasctl): a shortcut — a fresh orchestrator chat in your fleet-control folder (~/fleet-controlby default; override with--cwd, persist with--save) using the default agent, and because you ran it from a terminal it attaches by default.--no-attachfor a scriptable boot. This is the one-word front door to driving the fleet from your own shell.
termfleet control # boot + attach the orchestrator, here, now
termfleet ctl --cwd ~/fleet-control --save # remember the folder for next time
termfleet chat --attach --cwd ~/myproject # boot a chat here and stay attached
termfleet attach # reconnect to the one live session
termfleet attach claude # reconnect to the most-recent claude sessionDesign
tmux owns terminal sessions because it has stable IDs, reliable input/output
commands, capture APIs, and session persistence.
Provider instances own terminal/window/display state. The console owns board layout. Local and remote registries provide discovery and remote reachability without becoming the authority for terminal state.
Transport split:
- HTTP: health, bootstrap, debug, terminal capture.
- Socket.IO: provider commands, acknowledgements, snapshots.
- Raw WebSocket: live terminal PTY streams for the browser UI.
License
Apache-2.0. The standalone library is published as
@termfleet/core; the CLI +
console as termfleet.
