Fleet Commands
Fleet is what makes peering useful to an agent, not just to a human clicking around Settings: an agent driving one TermFlow instance can route a command to a paired machine and have it run there — "build this on the Mac" from the Windows box — and watch it happen.
Targeting precedence
A Fleet request can specify up to three targeting fields. TermFlow resolves them in a fixed order, and the first one present wins:
terminalId → machineId → targetOS → (none) local
| Field | Resolves to | Requirement |
|---|---|---|
terminalId | That exact terminal, wherever it lives | Requires a per-terminal Control grant on that terminal — see Grants & consent |
machineId | A specific paired machine | Requires that peer's Allow fleet commands consent to spawn a new terminal there |
targetOS | windows | macos | linux — the unique online peer running that OS | Same consent requirement; fails if the OS isn't uniquely matched (see below) |
| (none of the above) | Runs locally, exactly like a non-fleet command | — |
Naming an existing terminalId never creates anything new — it runs in a terminal that already exists, gated by the Control grant on that terminal. machineId and targetOS are the ones that can spawn a fresh terminal, which is why they're gated by the separate fleet-exec consent instead.
When targetOS doesn't resolve to exactly one machine
targetOS only works when there's a single unambiguous online peer running that OS:
| Situation | Result |
|---|---|
| More than one online peer matches the OS | HTTP 409 — ambiguous. Response includes the candidate machines plus the full roster. |
| No online peer matches the OS | HTTP 404 — no match. Response includes the full roster so the caller can pick something else. |
| Exactly one online peer matches | Request routes to that machine. |
The remote terminal Fleet spawns
When a machineId- or targetOS-targeted request needs a new terminal on the responder, that terminal is:
- Persistent — it isn't a throwaway subprocess; it behaves like any other terminal on that machine.
- Labeled
Fleet: <initiator name>, so the person at the responding machine can see exactly which peer spawned it and why. - Visible — it's a real tab in the responder's UI, not hidden or headless.
- Never auto-killed. It stays open until someone explicitly closes it: the responder's human, the initiator (via the API/MCP close call), or a peer revocation.
- Auto-grants the initiator Control on that specific terminal for the duration it's open — so follow-up commands can target it directly by
terminalIdwithout needing a fresh spawn. That Control grant is revoked when the terminal closes.
The long-poll execution model
execute on a Fleet request behaves differently from a plain local one: the responder holds the request open and only responds once the command finishes, or once the caller's timeoutMs elapses — whichever comes first.
- Command finishes first: the response comes back with
done: trueand the realexitCode. - Timeout elapses first: the response comes back with a live handle —
done: false,exitCode: null— plus the terminal's current screen. The command is still running on the responder; nothing was cancelled.
Completion itself is detected by a done-marker the responder injects around the command in the shell; the marker's own exit code is what's reported back as exitCode.
| Parameter | Type | Clamp | Default |
|---|---|---|---|
timeoutMs | number | [1000, 3,600,000] ms (1s – 1h) | 60000 (60s) |
Getting done: false back doesn't mean anything went wrong — it means the command outlived the wait window you gave it. Long builds, installs, or watch processes will routinely hit this. Don't treat it as an error; poll instead (see below).
Watching progress: get_terminal_screen
To check on a command after a timeout (or just to watch it live), call get_terminal_screen with the same terminalId (and machineId if remote). It returns the terminal's current live screen — the same authoritative, styled snapshot the UI itself renders — not the lossy, capped output history. This is the preferred way to observe an in-flight Fleet command; polling the plain output history can miss or garble full-screen output.
Worked example
An agent on the Windows machine wants to build a project on whichever paired Mac is online, without knowing its machine id in advance:
// 1. Discover the fleet
{ "name": "list_machines", "arguments": {} }
{
"machines": [
{ "machineId": "self", "deviceName": "win-dev-box", "os": "windows", "online": true, "self": true },
{ "machineId": "mac-mini", "deviceName": "mac-mini-studio", "os": "macos", "online": true, "self": false }
]
}
// 2. Route the build to the unique online macOS peer
{
"name": "execute_command",
"arguments": {
"targetOS": "macos",
"command": "cd ~/repo && bun run build",
"timeoutMs": 120000
}
}
// Timed out while the build was still running:
{
"machineId": "mac-mini",
"terminalId": "term_9f2a",
"deviceName": "mac-mini-studio",
"done": false,
"exitCode": null,
"screen": "…compiling module 214/300…"
}
// 3. Check on it later
{
"name": "get_terminal_screen",
"arguments": { "machineId": "mac-mini", "terminalId": "term_9f2a" }
}
{
"machineId": "mac-mini",
"terminalId": "term_9f2a",
"title": "Fleet: win-dev-box",
"running": true,
"screen": "…compiling module 289/300…"
}
Security notes
Fleet doesn't introduce a new trust model on top of peering — it composes the existing controls:
- The existing peering mTLS identity authenticates every request between machines.
- Per-terminal Control grants gate
terminalId-targeted execution. - The per-peer, default-deny "Allow fleet commands" consent gates spawning new terminals, and is re-checked live on every request.
- The initiator is auto-granted Control on any terminal Fleet spawns for it, revoked when that terminal closes.
- The command string is written to the responder's audit log.
- The spawned terminal is labeled and visible in the responder's own UI — never hidden.
There is deliberately no blocking confirmation dialog on the responder for an individual command — that would stall an unattended agent workflow. The grants, consent, labeling, and audit log above are the compensating controls instead.
Next steps
- Fleet API & MCP — the exact REST endpoints and MCP tool schemas behind everything on this page.
- Grants & consent — set up the grants and consent this page assumes are already in place.