# SSH remote worktrees

> SSH targets, relay grace periods, remote repo registration, remote PTY leases, agent-hook forwarding over SSH, and CLI selector constraints on paired runtimes.

- Repository: stablyai/orca
- GitHub: https://github.com/stablyai/orca
- Human docs: https://grok-wiki.com/public/docs/stablyai-orca-2036d532bf1c
- Complete Markdown: https://grok-wiki.com/public/docs/stablyai-orca-2036d532bf1c/llms-full.txt

## Source Files

- `src/shared/ssh-types.ts`
- `src/shared/types.ts`
- `tests/e2e/ssh-localhost.spec.ts`
- `src/cli/selectors.ts`
- `src/main/ipc/runtime-environments.ts`
- `AGENTS.md`

---

---
title: "SSH remote worktrees"
description: "SSH targets, relay grace periods, remote repo registration, remote PTY leases, agent-hook forwarding over SSH, and CLI selector constraints on paired runtimes."
---

Orca’s desktop app connects to remote hosts over SSH, deploys a versioned **relay** process on the server, and routes git, filesystem, and PTY operations through that relay. Remote repositories are tagged with a `connectionId` (the SSH target id); terminals run on the remote machine while the UI stays local. A separate **paired runtime** path (`orca` CLI with `--environment` or `--pairing-code`) talks to `orca serve` over WebSocket and imposes additional worktree selector rules—distinct from SSH wiring but documented here because the same CLI surfaces both modes.

```mermaid
flowchart LR
  subgraph local["Local (Electron main)"]
    UI["Renderer / store"]
    IPC["ipc/ssh + repos:addRemote"]
    Session["SshRelaySession"]
    Hooks["agentHookServer.ingestRemote"]
  end
  subgraph ssh["SSH transport"]
    Conn["SshConnection"]
    Mux["SshChannelMultiplexer"]
  end
  subgraph remote["Remote host"]
    Relay["orca relay"]
    PTY["Remote PTYs + git/fs"]
  end
  UI --> IPC --> Session
  Session --> Conn --> Mux --> Relay
  Relay --> PTY
  Relay -->|agent.hook notification| Mux --> Hooks --> UI
```

## SSH targets

An SSH target is a persisted `SshTarget` record (settings UI and `window.api.ssh.*`). Required fields are `id`, `label`, `host`, `port`, and `username`. Optional fields mirror OpenSSH config resolution:

| Field | Role |
| --- | --- |
| `configHost` | Host alias for `ssh -G` lookup (defaults to `label` on load if missing) |
| `identityFile` / `identityAgent` / `identitiesOnly` | Key-based auth |
| `proxyCommand` / `jumpHost` | ProxyJump / ProxyCommand |
| `relayGracePeriodSeconds` | Relay shutdown delay after disconnect (see below) |
| `lastRequiredPassphrase` | Whether the last connect needed a credential prompt (startup reconnect partitioning) |
| `portForwards` | Saved local→remote forwards restored on connect |

Preload IPC covers target CRUD, connect/disconnect, relay reset, port forwards, remote directory browse, and credential prompts (`ssh:addTarget`, `ssh:connect`, `ssh:disconnect`, `ssh:resetRelay`, `ssh:terminateSessions`, etc.).

<Note>
SSH targets are **not** runtime environments. Pairing a CLI to `orca serve` uses saved environments and pairing codes (`runtimeEnvironments:*`), documented on the runtime environments page.
</Note>

## Connection and relay lifecycle

Connect serializes per target (`connectInFlight`) so two concurrent `ssh:connect` calls cannot spawn duplicate relay sessions.

| `SshConnectionStatus` | Meaning |
| --- | --- |
| `connecting` | SSH handshake in progress |
| `auth-failed` | Authentication failed |
| `deploying-relay` | Relay binary deploy / attach |
| `connected` | SSH up and relay ready |
| `reconnecting` | Relay channel lost; bounded backoff retry |
| `reconnection-failed` | Backoff exhausted |
| `disconnected` / `error` | Idle or terminal failure |

`SshRelaySession.establish()` deploys the relay, verifies it with `session.resolveHome`, registers providers (PTY, filesystem, git), installs remote agent hooks when enabled, reattaches known PTYs, then sends `relay.configureGraceTime`. `detach()` on `ssh:disconnect` tears down local providers but **does not** kill remote PTYs—the relay grace timer keeps them alive. `dispose()` / explicit terminate paths mark leases `terminated` and stop remote processes.

Relay channel loss while SSH stays up triggers exponential backoff (base 500 ms, max 15 s, up to six attempts) before surfacing `reconnection-failed`. Version mismatch is terminal (no backoff).

Runtime RPC exposes `ssh.getState` and `ssh.connect` for headless callers that share the same registered handlers.

## Relay grace periods

Grace time controls how long the relay keeps remote PTYs (and workspace state) after Orca disconnects.

| Constant | Value |
| --- | --- |
| `DEFAULT_SSH_RELAY_GRACE_PERIOD_SECONDS` | 10 800 (3 hours) |
| `MIN_SSH_RELAY_GRACE_PERIOD_SECONDS` | 60 |
| `MAX_SSH_RELAY_GRACE_PERIOD_SECONDS` | 604 800 (7 days) |
| `SSH_RELAY_CONFIGURE_GRACE_TIME_METHOD` | `relay.configureGraceTime` |

Behavior:

- **Unset** on a target → default 3 hours at configure time.
- **`0`** → relay stays up until manual reset (“keep alive until reset” in settings). Host sleep calls `prepareForHostSleep()` and pushes grace `0` immediately.
- **Other values** → clamped to \[60, 604 800\] before notify (except `0`).

Settings validate the same bounds. Legacy persisted fields `remoteWorkspaceSyncGracePeriodSeconds` migrate into `relayGracePeriodSeconds` on load.

## Remote repo registration

`repos:addRemote` registers a repository on a **connected** SSH target:

<ParamField body="connectionId" type="string" required>
SSH target id returned from `ssh:addTarget` / `ssh:connect`.
</ParamField>
<ParamField body="remotePath" type="string" required>
Absolute path on the remote host, or `~` / `~/…` (resolved via `session.resolveHome` on the relay).
</ParamField>
<ParamField body="displayName" type="string">
Optional display name; for `~/` without a name, the SSH target label is used.
</ParamField>
<ParamField body="kind" type="'git' | 'folder'">
Default `git`. Non-git paths return an error unless `kind: 'folder'` (same UX as local add-repo).
</ParamField>

The handler requires `getSshGitProvider(connectionId)`. Duplicate `(connectionId, path)` returns the existing repo. Successful registration sets `Repo.connectionId` and notifies the relay with `session.registerRoot` so filesystem ACLs scope writes to that workspace root.

Worktrees for remote repos are listed and managed like local ones, but git/fs/terminal operations dispatch through SSH providers keyed by `connectionId`.

## Remote PTY identity and leases

Relay-local PTY ids (for example `pty-1`) are not globally unique. The app encodes them as:

```text
ssh:{encodeURIComponent(connectionId)}@@{relayPtyId}
```

Helpers `toAppSshPtyId`, `toRelaySshPtyId`, and `parseAppSshPtyId` translate between app-facing and relay-facing ids.

**`SshRemotePtyLease`** records are persisted in store state (`sshRemotePtyLeases`) per target:

| Field | Purpose |
| --- | --- |
| `targetId`, `ptyId` | Target-local relay id (storage normalizes app ids back to relay ids) |
| `worktreeId`, `tabId`, `leafId` | UI binding for reattach |
| `state` | `attached` \| `detached` \| `terminated` \| `expired` |
| `createdAt`, `updatedAt`, `lastAttachedAt`, `lastDetachedAt` | Timestamps |

Lifecycle highlights:

- **Disconnect** → leases → `detached`; remote PTYs survive in the relay.
- **Reattach** (`reattachKnownPtys`) → merges in-memory ownership with durable leases inside the grace window; success → `attached`, missing PTY → `expired` and synthetic `pty:exit`.
- **PTY exit** → `terminated`.
- **`ssh:terminateSessions`** → kills remote PTYs; may require reconnect first (`SSH_TERMINATE_RECONNECT_REQUIRED`) when disconnect left preserved sessions.

Remote terminals receive Orca env vars including `ORCA_PANE_KEY` (`tabId:leafId`), `ORCA_TAB_ID`, `ORCA_WORKTREE_ID`, and agent-hook variables when hooks are enabled.

## Agent-hook forwarding over SSH

Remote agent status is on by default. Set `ORCA_FEATURE_REMOTE_AGENT_HOOKS=0` (or empty) to opt out.

<Steps>
<Step title="Relay and main process wire-up">
On connect, `SshRelaySession` installs managed hook configs on the remote home (SFTP), ships OpenCode/Pi plugin sources via `agent_hook.installPlugins`, and subscribes to mux notifications.
</Step>
<Step title="PTY → relay → Orca">
Agents POST to `http://127.0.0.1:${ORCA_AGENT_HOOK_PORT}/hook/<source>` on the **remote** loopback. The relay normalizes payloads and sends JSON-RPC notification `agent.hook` with `connectionId: null` on the wire.
</Step>
<Step title="Trust boundary ingest">
`agentHookServer.ingestRemote()` re-validates the payload and stamps the real `connectionId` from the SSH target id. Renderer agent status and `agentStatus.onSet` events include `connectionId` and `worktreeId` for remote panes.
</Step>
<Step title="Reconnect replay">
After wiring, Orca calls `agent_hook.requestReplay` so cached per-`paneKey` payloads survive channel gaps.
</Step>
</Steps>

Injected PTY environment (when hooks are active) includes `ORCA_AGENT_HOOK_PORT`, `ORCA_AGENT_HOOK_TOKEN`, `ORCA_AGENT_HOOK_ENV`, `ORCA_AGENT_HOOK_VERSION`, plus pane/worktree keys. Plugin overlays use `OPENCODE_CONFIG_DIR` and `PI_CODING_AGENT_DIR` paths materialized on the remote.

<Warning>
Treat remote hook POST bodies as untrusted: `ingestRemote` re-runs canonical normalizers at the SSH boundary before updating main-process state.
</Warning>

## Port forwards and detected ports

`SavedPortForward` on the target restores forwards after reconnect. `PortForwardEntry` ties forwards to `connectionId` and can carry `advertisedUrl` / `advertisedProtocol` when a remote PTY prints dev-server URLs; the main process rewrites them to local forwarded ports for the embedded browser.

## CLI selector constraints on paired runtimes

When the CLI sets `RuntimeClient.isRemote` (via `ORCA_PAIRING_CODE`, `ORCA_REMOTE_PAIRING`, or `ORCA_ENVIRONMENT`), the **client machine’s cwd is not the server’s filesystem**. Selectors and defaults change accordingly:

| Surface | Local behavior | Remote (`isRemote`) constraint |
| --- | --- | --- |
| `active` / `current` worktree | Resolves enclosing worktree from cwd | **Rejected** — use `id:`, `branch:`, `issue:`, or `path:` with **server-absolute** paths |
| `terminal create` | Can infer worktree from cwd | Requires `--worktree` |
| `file` commands | Can default worktree from cwd | Requires explicit `--worktree` |
| `repo add --path` | Resolves relative to cwd | Path must be absolute on the remote server |
| Browser/computer targets | Auto-resolve worktree from cwd | Omit `--worktree` to use server-side focus, or pass explicit server selectors |

Automations can also target `{ type: 'ssh', connectionId }` for external scheduler integrations—separate from CLI pairing but sharing the same SSH target ids.

<Info>
Desktop SSH worktrees do not require CLI remote pairing. A local `orca` CLI against your laptop runtime still uses `active`/`current`; only WebSocket-paired invocations hit the remote selector rules.
</Info>

## Verification

Localhost SSH E2E is gated and documents expected signals:

| Variable | Purpose |
| --- | --- |
| `ORCA_E2E_SSH_LOCALHOST=1` | Enable `tests/e2e/ssh-localhost.spec.ts` |
| `ORCA_E2E_SSH_HOST`, `ORCA_E2E_SSH_PORT`, `ORCA_E2E_SSH_USER` | Target host |
| `ORCA_E2E_SSH_CONFIG_HOST`, `ORCA_E2E_SSH_IDENTITY_FILE` | OpenSSH alias / key |
| `ORCA_FEATURE_REMOTE_AGENT_HOOKS` | Unset or non-`0` for hook/pane-key tests |

The test connects with `relayGracePeriodSeconds: 1`, registers a remote repo, verifies terminal markers, `ORCA_PANE_KEY` / hook env, Codex hook → renderer status, and Ctrl+C / Escape interrupt semantics.

## Related pages

<CardGroup>
<Card title="Worktrees and repos" href="/worktrees-and-repos">
Repo registration, worktree create/list/remove, and `connectionId` on `Repo`.
</Card>
<Card title="Terminals and agents" href="/terminals-and-agents">
PTY lifecycle, `TuiAgent` detection, `ORCA_*` env, and terminal CLI vs orchestration.
</Card>
<Card title="Runtime environments" href="/runtime-environments">
`orca serve`, pairing codes, and `--environment` targeting (CLI remote mode).
</Card>
<Card title="Selectors and JSON output" href="/selectors-json-output">
Selector grammar, `--json`, and remote-runtime resolution errors.
</Card>
<Card title="Scheduled automations" href="/scheduled-automations">
Cron automations with `ssh` execution targets.
</Card>
<Card title="Testing" href="/testing">
Vitest, Playwright E2E, and SSH localhost env flags.
</Card>
<Card title="Troubleshooting" href="/troubleshooting">
Runtime reachability, selector ambiguity, and SSH relay failures.
</Card>
</CardGroup>
