> For the complete documentation index, see [llms.txt](https://docs.gotempest.app/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://docs.gotempest.app/integrations/tempest-remote-plugin-protocol.md).

# Tempest Remote — Build Your Own Integration

Build editor, terminal, tunnel and automation integrations using Tempest CLI's remote protocol v1, authentication prompts, ProxyCommand attach and tunnel lifecycle.

Tempest Remote lets a local plugin connect ordinary SSH tools to servers saved in Tempest. Your plugin selects a server and presents authentication prompts; Tempest CLI resolves the saved credentials, proxies and jump hosts, authenticates upstream, and returns an SSH host alias and connection session ID. Your editor or SSH client then connects through that alias.

The desktop app does not need to remain open. This interface is usable outside VS Code: it is a CLI control protocol and a local SSH service. VS Code's Tempest Remote extension is one consumer, using Microsoft Remote - SSH to install and run VS Code Server.

This document describes **protocol v1**. Check `protocol: 1` in the installed executable's capabilities. Protocol v1 includes the control and pipe commands.

For Node.js integrations, use the MIT-licensed [Tempest Remote SDK](/integrations/tempest-remote-sdk.md), package `@tempest-term/remote-sdk`, instead of implementing another NDJSON client. Its `ControlClient` is shared by the VS Code and Codex integrations; your host still supplies its own UI and SSH workspace behavior. [SDK source](https://github.com/tempest-term/tempest-remote-sdk).

## Architecture

```mermaid
flowchart LR
    Plugin[Local plugin] <-->|NDJSON: requests and prompts| Control[Tempest control]
    Control --> Vault[Accounts and encrypted vault]
    Control --> Relay[SSH relay owned by control]
    Client[Editor / OpenSSH] <-->|SSH bytes over stdin/stdout| Pipe[Tempest ProxyCommand]
    Pipe <-->|Authenticated session attach| Control
    Control <-->|SSH with local client key| Relay
    Relay <-->|Authenticated SSH channels| Server[Saved target via proxies / jump hosts]
```

There are two distinct transports:

* **Control:** UTF-8 newline-delimited JSON on the CLI's stdin/stdout. Used for discovery, setup and authentication UI.
* **Data:** standard SSH over the ProxyCommand's stdin/stdout. `tempest remote pipe` attaches to an authenticated loopback endpoint owned by control, which bridges the bytes to its SSH relay. Used for commands, terminals and TCP forwarding. SSH bytes never go through the NDJSON control pipe.

The SSH alias configures a ProxyCommand that attaches to the control session. Control establishes and owns the upstream tunnel; the pipe never authenticates to the target or creates a second upstream session. Remote credentials are not exported to your plugin or OpenSSH configuration.

## Prerequisites

Run locally, as the same OS user who owns the Tempest account:

```bash
tempest remote --help
tempest login
tempest account list
tempest ssh list
```

The CLI must expose both `remote control` and `remote pipe`. Save the SSH server in Tempest first. Remote requires a vault policy allowing TCP forwarding, even if your integration only intends to execute commands. It does not require a `portForwarding` subscription entitlement. `ssh-keygen` must be available to prepare the local keys; OpenSSH is needed for the command examples below.

Multiple accounts are selected using `tempest account use <ID|EMAIL>`. This changes the desktop app's selected account too. Protocol v1 has no account-selection method or login method. Complete initial vault password setup in Tempest before using the protocol.

## Connection sequence

1. Spawn `tempest remote control --protocol 1` with piped stdin/stdout and an argument array, without a shell. Use a detached process if your host must exit while the relay continues.
2. Send `capabilities` and check `result.protocol === 1`.
3. Send `servers.list`; answer any vault unlock prompts and show the returned server choices.
4. Send `remote.ensure` with the selected opaque `serverId`. Answer authentication and host-key consent prompts until the response arrives.
5. Pass the returned `sshHost` to OpenSSH, using the same effective SSH config supplied during setup. The generated ProxyCommand contains the session ID returned by control; OpenSSH starts the pipe process and attaches it to that session.
6. Once setup requests have completed successfully, close control stdin and release the process without killing it. Control retains the authenticated tunnel during handoff; attached data pipes then drive its lifetime. A handoff without an attach expires after 120 seconds.

Do not close stdin before receiving the setup response. Do not wait for the CLI process to exit as a readiness signal: it can remain alive for the entire SSH session.

## Wire format

Every frame is one JSON object followed by a newline. Parse bytes incrementally: a pipe chunk can contain part of a frame or several frames. Decode UTF-8 after collecting a complete line. Do not treat stderr as protocol data.

Use string IDs of at most 128 characters. Request IDs correlate responses; prompt IDs correlate prompt answers and belong to a separate interaction. Keep outgoing frames at or below **65,536 bytes including the newline**. Oversized, incomplete, invalid JSON or invalid envelope frames can close control input instead of producing an error response. Bound your own incoming buffers too; the CLI's input limit is not an advertised maximum size for `servers.list` output.

The CLI executes requests serially, with a bounded queue of eight waiting requests. Send requests sequentially. Keep reading stdout while waiting for a response so authentication prompts can be answered.

### Methods

```json
{"type":"request","id":"1","method":"capabilities","params":{}}
{"type":"response","id":"1","result":{"protocol":1}}
```

| Method          | Parameters                                                     | Result                                                         |
| --------------- | -------------------------------------------------------------- | -------------------------------------------------------------- |
| `capabilities`  | `{}`                                                           | `{ "protocol": 1 }`                                            |
| `servers.list`  | `{}`                                                           | Array of server entries shown below.                           |
| `remote.ensure` | `{ "serverId": "...", "configFile": "/absolute/path/config" }` | `{ "sshHost": "tempest-...", "sessionId": "<connection-id>" }` |

`configFile` is optional; the default is `~/.ssh/config`. Supply an absolute path for a custom config, and make sure the consumer uses that same file, for example with `ssh -F`. The CLI creates/updates it with a managed Include, preserving user entries outside the managed block.

A server entry looks like:

```json
{"id":"opaque-server-id","label":"Build server","description":"build.example:22"}
```

Treat `id` as an opaque string. Its current encoding carries account, vault and saved-server scope, but plugins should not decode or manufacture it. `remote.ensure` accepts at most 4,096 characters for `serverId`. Persist the returned ID if needed, and refresh discovery when its saved scope becomes unavailable. Labels and descriptions are display text, not identifiers or shell arguments.

Example setup:

```json
{"type":"request","id":"3","method":"remote.ensure","params":{"serverId":"opaque-server-id"}}
{"type":"response","id":"3","result":{"sshHost":"tempest-<stable-hash>","sessionId":"<connection-id>"}}
```

The alias is stable for the saved identity; the session ID identifies a particular live control tunnel and changes when a replacement is created. New connections receive a readable alias such as `tempest-ubuntu-cloudnet-6e0f3044c311`, using the saved name and a short identity suffix. Existing connections retain their assigned names. Consumers must treat `sshHost` as an opaque SSH alias, not parse a fixed-length hash. Session IDs still use 64 lowercase hexadecimal characters. `remote.ensure` can reuse a live session after an authenticated attach-endpoint check. It does not guarantee new upstream authentication on every call. Ensuring an idle live session reserves up to 120 seconds for handoff; ensuring a session with active pipes does not extend its later idle lifetime. Always ensure again before opening a saved/recent connection; a stored alias alone does not start the relay.

### Prompts

A prompt can arrive before the final response to either discovery or setup:

```json
{"type":"prompt","id":"p1","kind":"password","message":"Unlock your Tempest vault"}
{"type":"prompt_response","id":"p1","value":"<user-input>"}
```

| Kind emitted by the current CLI | Answer                         | UI behavior                                                                      |
| ------------------------------- | ------------------------------ | -------------------------------------------------------------------------------- |
| `password`                      | String                         | Hidden input for vault passwords, key passphrases, PINs and non-echo challenges. |
| `text`                          | String                         | Visible input for usernames and echo-enabled keyboard-interactive challenges.    |
| `confirm`                       | JSON boolean `true` to consent | Explicit user confirmation for host keys or security-key signing.                |

The VS Code transport also understands `select` with choice IDs, but the current Rust endpoint does not emit it. Do not depend on it for account selection.

Host-key prompts include host, algorithm, fingerprint and trust state. Show the complete message, including changed-key information, and let the user verify it. Never automatically approve. Security-key confirmation may be followed by a physical touch operation. Keyboard-interactive authentication may ask multiple questions across several rounds.

Cancel an active prompt explicitly:

```json
{"type":"prompt_response","id":"p1","cancelled":true}
```

Each CLI prompt waits up to five minutes. Cancellation normally results in the affected request returning `cancelled`. There is no general `request.cancel` or `remote.stop` method in v1. Before successful handoff, a plugin can terminate its setup process to cancel a connection attempt. After handoff, closing the control pipe leaves the relay running; it is not a disconnect API.

Do not log full frames, prompt answers, secrets or keys. Keep passwords out of argv, shell commands and persistent plugin settings. Unknown prompt kinds should fail safely rather than be approved or answered automatically.

### Errors

```json
{"type":"response","id":"3","error":{"code":"forwarding_forbidden"}}
```

| Code                               | Suggested action                                                                                      |
| ---------------------------------- | ----------------------------------------------------------------------------------------------------- |
| `local_state_unavailable`          | Check local Tempest state and permissions.                                                            |
| `account_unavailable`              | Check sign-in and selected account.                                                                   |
| `vault_unavailable`                | Unlock the vault or finish initial setup.                                                             |
| `catalog_unavailable`              | Retry discovery after checking vault access.                                                          |
| `vault_scope_changed`              | Refresh the server list and select again.                                                             |
| `saved_server_unavailable`         | Check the saved server and referenced credentials; refresh discovery.                                 |
| `upstream_connection_failed`       | Check connectivity, authentication and host-key consent.                                              |
| `forwarding_forbidden`             | Check the vault's TCP-forwarding policy. It is checked again on attach and when opening SSH channels. |
| `remote_key_unavailable`           | Check private-file permissions and `ssh-keygen`.                                                      |
| `ssh_config_unavailable`           | Check the effective SSH config path and write access.                                                 |
| `cancelled`                        | End the interaction without an alarming failure notification.                                         |
| `remote_failed` or an unknown code | Show a generic failure with a retry option.                                                           |

Map codes to your own UI messages. The protocol intentionally omits arbitrary upstream diagnostics. An unsupported method also becomes `remote_failed`; an unsupported protocol version fails at startup. Handle spawn errors, EOF, malformed responses and process exit, rejecting pending requests and cleaning up setup processes. Use finite client timeouts that allow for interactive authentication and network setup.

## Plugin adapter example

The following JavaScript shows the host integration flow. `control` is your NDJSON adapter implementing the framing, response correlation, prompt handling and lifecycle rules above; this is not a complete transport implementation.

```js
async function connectSavedServer(control, ui, sshConfigFile) {
  const capabilities = await control.request('capabilities', {});
  if (capabilities.protocol !== 1) {
    throw new Error('A CLI with remote pipe attach support is required');
  }

  const servers = await control.request('servers.list', {});
  const selected = await ui.pickServer(servers);
  if (!selected) {
    control.cancelSetup();
    return;
  }

  const params = { serverId: selected.id };
  if (sshConfigFile) params.configFile = sshConfigFile;
  const { sshHost, sessionId } = await control.request('remote.ensure', params);

  // No outstanding requests: end stdin, release pipes and unref the child.
  // Its generated ProxyCommand attaches by session ID and drives tunnel lifetime.
  // Do not kill control during the window handoff.
  control.handoff();
  return { sshHost, sessionId };
}

async function answerPrompt(frame, ui, send) {
  let value;
  switch (frame.kind) {
    case 'password': value = await ui.hiddenInput(frame.message); break;
    case 'text': value = await ui.textInput(frame.message); break;
    case 'confirm': value = await ui.confirm(frame.message); break;
    default: throw new Error('Unsupported prompt kind');
  }
  send(value === undefined
    ? { type: 'prompt_response', id: frame.id, cancelled: true }
    : { type: 'prompt_response', id: frame.id, value });
}
```

Here `undefined` means user cancellation; `false` means explicit refusal. A `false` confirmation rejects consent, but need not produce the `cancelled` code. Serialize prompt UI interactions and keep it responsive while a request is pending. Launch the executable with `spawn(executable, ['remote', 'control', '--protocol', '1'], { shell: false, detached: true, stdio: ['pipe', 'pipe', 'ignore'] })`, or the equivalent in your platform. Release all parent process/pipe references during handoff so the editor can exit.

Store an alias-to-server-ID mapping if your host opens recent connections. On reopen, start a fresh control process and ensure the saved ID before handing the alias to its SSH consumer. Host-specific remote-workspace discovery and bootstrap remain your plugin's responsibility.

## Using the resulting SSH host

After setup, the following are ordinary OpenSSH operations. Replace the illustrative alias with the exact returned value. For a custom config, add `-F /absolute/path/config` to every command.

```bash
# Non-interactive command, with separate stdout/stderr and exit status
ssh 'tempest-<stable-hash>' 'uname -a'

# Interactive terminal with PTY
ssh -t 'tempest-<stable-hash>'

# Forward a local port to a service reachable from the SSH target
ssh -N -o ExitOnForwardFailure=yes -L 127.0.0.1:15432:127.0.0.1:5432 'tempest-<stable-hash>'
```

In production, pass the alias and options as an argument array. Remote command strings are interpreted by the target shell; escape user-controlled paths or use a fixed remote helper that reads structured input on stdin. Shell-free local spawning does not escape remote shell arguments.

OpenSSH reads the generated ProxyCommand, identity and pinned host-key configuration automatically. The command has this form:

```
<CLI executable> remote pipe --host tempest-<stable-hash> --session <connection-id>
```

The CLI quotes its executable for the ProxyCommand shell, including paths with spaces. Prefer the generated config instead of reconstructing that shell command. An SSH library without OpenSSH-config support must support an external stdin/stdout transport and start this pipe, then honor the generated user, identity and pinned known-hosts entry. Connecting directly to a generated host/port bypasses attach ownership and is not an equivalent integration. There is no v1 method returning the SSH options individually; do not derive ports or treat private state/descriptors as a public API.

## SSH support and lifecycle

| Feature                                                     | Current relay                                          |
| ----------------------------------------------------------- | ------------------------------------------------------ |
| Session channels: exec and interactive shell                | Supported.                                             |
| PTY request and window resize                               | Supported.                                             |
| Environment requests before command start                   | Forwarded; upstream server can reject them.            |
| Binary stdin/stdout, stderr, EOF and exit status            | Supported.                                             |
| `direct-tcpip` channels, including OpenSSH local forwarding | Supported, subject to policy and upstream permissions. |
| Remote forwarding (`ssh -R`)                                | Unsupported.                                           |
| Unix-domain socket forwarding                               | Unsupported.                                           |
| SFTP subsystem                                              | Unsupported.                                           |
| X11 and SSH agent forwarding                                | Unsupported.                                           |
| Mosh and saved terminal startup commands                    | Disabled for this connection flow.                     |

Modern `scp` commonly requires SFTP, so do not promise `scp` or SFTP compatibility. Use Tempest's dedicated file-transfer interfaces when a plugin needs file operations. A command can stream data through exec/stdin, but any file protocol, quoting and failure recovery then belongs to that integration.

The attach endpoint bounds concurrent accepted connections, including handshakes and side requests, to 64. The underlying SSH relay bounds local SSH connections to 32 and channels per connection to 128. These are implementation limits, not negotiated capabilities.

Control owns authentication, the relay and the upstream tunnel. The pipe only attaches and transfers bytes. Lifetime follows these rules:

* Before the first attach, control allows **120 seconds** for handoff. Ordinary endpoint probes do not extend that allowance.
* An authenticated data pipe keeps the session alive even without traffic. The last pipe's disconnection starts a **three-second reconnect grace**; a replacement attach within that interval can continue using the tunnel.
* Pipe stdin EOF half-closes its data socket to preserve trailing output, with a **five-second drain bound**. If control closes its sockets, pipes exit even when their stdin remains open.
* On Unix, proxy **SIGHUP, SIGINT and SIGTERM** are passed over a separate authenticated side connection, identifying the session and pipe. Control closes that attach; if it was the last one, it tears down the tunnel immediately and exits with `128 + signal`. The proxy preserves the same signal exit status.
* OpenSSH can send SIGHUP when normally cleaning up a ProxyCommand. A proxy signal must address its own pipe so other attached consumers remain usable. Signalling the control process itself closes all of its attaches.

Terminal Ctrl+C and PTY resizing remain SSH protocol traffic. Local process-signal forwarding does not inject JSON into the SSH stream or send POSIX signals to remote commands. Windows uses pipe/EOF cleanup; Unix signal forwarding is platform-specific.

Interrupted commands are not replayed. Retry setup after upstream failure, and let the user decide whether to rerun operations with side effects.

Multiple consumers can attach to the same live session. Setup uses file locks to serialize simultaneous ensures across processes; plugin cancellation must not assume exclusive ownership of a shared control. Successful attach and idle expiry are serialized. Reuse requires an authenticated endpoint exchange, rather than TCP reachability alone; the final pinned-key SSH handshake remains required. Successful ensure does not guarantee that the upstream will remain healthy.

## Local files and trust boundaries

Managed artifacts live under `~/.tempest/remotes/`: per-alias host/client keys, pinned `known_hosts`, locks and generated `.conf` files. Each live `session-<id>.json` descriptor contains a loopback endpoint and an unguessable attach capability. `state.json` is a pointer to the latest session ID, not an upstream-credential store. Control removes its session descriptor on orderly shutdown; stale IDs after a crash must be refreshed through `remote.ensure`. Unix directories use mode 0700 and private files 0600; Windows uses restricted user ACLs. Do not change these permissions or publish the files.

The attach endpoint checks the session ID and private capability before accepting data or signal requests. The underlying relay also requires its matching client public key; loopback access or the public session ID alone is insufficient. The capability is never put in ProxyCommand argv or control results. Rust separately verifies host keys for the target and jump hosts. Anyone able to read the private attach capability and local client key can use the session while it is alive, so the plugin must run within the user's local trust boundary.

The CLI installs an idempotent `# BEGIN TEMPEST REMOTES` Include block at the start of the effective SSH config. Use `remote.ensure` to manage it; do not rewrite generated entries yourself.

A separate SSH config can keep integration settings in a file you control. It still points to Tempest-managed artifacts and does not create an isolated account or isolated upstream relay.

## What else can be built?

These are integration ideas enabled by the current channel support, not additional shipped Tempest products:

| Integration                           | How to build it                                                                         | Boundary to account for                                                                                                               |
| ------------------------------------- | --------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------- |
| Other editor plugins                  | Pick saved servers, ensure the alias, hand it to the editor's SSH transport.            | The editor owns remote-agent installation and workspace behavior; check whether it needs unsupported subsystems or socket forwarding. |
| Terminal launchers                    | Open a PTY SSH session using a saved server.                                            | Provide prompts locally and ensure again when reopening.                                                                              |
| Database and internal-service tunnels | Run `ssh -N -L` through the alias for PostgreSQL, Redis, dashboards or APIs.            | Bind local listeners to loopback; target services still require their own authentication.                                             |
| Remote builds and test runners        | Use exec channels, stream logs and collect exit status.                                 | Supply your own job lifecycle; interrupted jobs are not automatically resumed.                                                        |
| Debugger adapters                     | Launch a remote debug server with exec and reach its TCP port through local forwarding. | Own debugger bootstrap and restrict access to its listener.                                                                           |
| Git integrations                      | Use the alias for Git-over-SSH commands where the remote server supports them.          | Requires a suitable saved SSH target/user and repository path; no SFTP dependency.                                                    |
| Data-science tools                    | Start a notebook or worker over exec, then tunnel its HTTP/TCP endpoint.                | Manage startup, shutdown and application authentication yourself.                                                                     |
| User-supervised automation            | Reuse saved connection authentication for explicit remote operations.                   | Interactive prompts can recur; this is not an unattended credential-export API.                                                       |

A useful first extension is a **saved-service tunnel picker**: select a Tempest server, choose a service port, start a loopback-bound forward, display its local endpoint and stop the owned OpenSSH process when the user disconnects. It reuses authentication without requiring a remote editor agent.

## Integration validation

Before distributing a plugin, verify: locked-vault prompts; new and changed host-key refusal; keyboard-interactive and security-key flows where applicable; cancellation and retry; separate stderr/exit status; binary stdin and EOF; local forwarding and half-close; custom config paths with spaces; handoff after the original host exits; startup expiry and reconnect grace; control exit with pipe stdin still open; rejected/stale session attach; Unix signal forwarding, including OpenSSH cleanup SIGHUP; simultaneous consumers without one proxy signal disconnecting another; and account/vault switching. Test the distributed CLI and plugin on each platform you support. Loopback relay tests alone do not establish editor bootstrap compatibility.

## See also

* [Tempest Remote for VS Code](/integrations/tempest-remote-editors.md#vs-code)
* [Tempest CLI](/cli/tempest-cli.md)
* [Accounts and vaults](/accounts-vaults-and-privacy/accounts-and-vaults.md)
* [SSH port forwarding](/connections-and-ssh/ssh-port-forwarding.md)
* [Install Tempest MCP Server](/ai-and-automation/install-tempest-mcp-server-in-ai-clients.md)
