--- title: Remote Link description: Connect an OpenCodex Home computer to a Child computer over SSH. --- A machine link connects an OpenCodex **Home** computer to a **Child** computer over SSH. The Home serves the Child through the SSH tunnel, while both computers keep their local OpenCodex service on port `10100`. The dashboard transfers the per-child link key through SSH, so you do not type a token. ## Requirements - The Home computer can log in to the Child with an OpenSSH key. - For a Child-initiated link, the Child can log in to Home with an OpenSSH key (password login is not supported). - OpenCodex 2.66.0 or later on Home (and on the Child for a Child-initiated link or full Child install). When adding a Child from Home, a bare SSH host needs no pre-installed Node, Bun, or OpenCodex if Home has a verified `ocx-child` build for the Child's platform: Home can then plant that standalone binary over SSH during onboarding. The npm package and the desktop app do not include `ocx-child` builds; see [Ephemeral Child machines](#ephemeral-child-machines-ocx-child) for where Home looks for one. - Both computers run macOS, Linux, or Windows x64. Windows requires OpenSSH Server and Windows PowerShell; Git Bash and curl are not required. - The dashboard that adds a Child from Home is opened on Home itself or through a paired Hub session. Turning the current computer into a Child requires an operator-paired dashboard session; a credentialless local dashboard session cannot commit that routing change. Password SSH is not supported. Links can be started from Home or Child. Windows execution uses PowerShell over the same pinned OpenSSH connection. ## Add a Child from `#remote` 1. Open the dashboard at `#remote` and switch Remote Link on. 2. Choose **Home**, then **Continue**. The SSH host list opens. 3. Choose a host from the SSH candidates, or enter an SSH config alias. 4. Run the connection test and compare the offered host fingerprint with the fingerprint for the machine you intend to use. Comparing it helps detect a wrong host or a changed host key before SSH trusts the host. 5. Confirm the fingerprint, then choose **Connect child** (or **Plant lightweight agent & connect** when connecting a bare SSH host that does not have OpenCodex installed yet). The dashboard does not ask you to enter a token. It probes the host first, and it cannot apply the link until you explicitly confirm the fingerprint. The Add Child sheet explains that the Child uses this Home's providers over SSH and lists the prerequisites above. Host aliases come from `~/.ssh/config` on the Home running OpenCodex, not necessarily the computer displaying the browser. If discovery succeeds with no hosts, add a `Host` entry like this, then choose **Rescan hosts**. You can also enter an existing alias manually; selecting or entering an alias enables **Test connection**. The sheet links to this guide. Host discovery also follows top-level `Include` directives. Parentheses and braces in an included filename stay literal when combined with wildcards: for example, `Include conf.d/*(1).conf` finds `backup(1).conf`, and `Include conf.d/shard-{1..3}-*.conf` looks for filenames containing `{1..3}`. An `Include` inside a `Host` or `Match` block is conditional and is not used for discovery. ```sshconfig Host devbox HostName devbox.example.com User you IdentityFile ~/.ssh/id_ed25519 ``` If discovery fails, the sheet shows **Could not load SSH hosts**, the available request reason and **Retry**, rather than claiming the host list is empty. A connection-test failure keeps its specific reason and sanitized SSH hint in the active sheet, not duplicated behind it. Check the reason, use the SSH diagnostic in Troubleshooting below when relevant, and retry. Rescanning keeps the entered alias but requires fresh fingerprint review before connecting. ## Connect this computer as a Child On the computer that should use the Home's providers: 1. Open the dashboard at `#remote` and switch Remote Link on. 2. Choose **Child**. The SSH host list opens. 3. Choose the Home's SSH host, run the connection test, then compare and confirm its host fingerprint. 4. Read the notice and choose **Connect as Child**. Connecting restarts OpenCodex on this computer. Codex turns that are already running finish first, and new requests can fail for up to a minute while it restarts. The dashboard then reloads by itself and shows the Child link. Codex keeps using `http://127.0.0.1:/v1` on this computer, with no token and no environment variable to set: the local OpenCodex relays each request to the Home, which serves it with its own providers and accounts. The Child waits for its configured port while the old process releases it. If a CLI-managed restart still fails, run `ocx start` on the Child and check `~/.opencodex/restart-handoff.log`. In the desktop app, the app starts and supervises the replacement automatically. An explicit `ocx stop` on a Remote Link Child stops its proxy and, when shared restoration succeeds, restores native Codex routing, its catalog and backed-up resume-history metadata and removes the managed Grok configuration. The saved Home connection is kept; use `ocx disconnect` to remove that connection. If a hub connection takes ownership while stopping, its routing and configuration are preserved. A connection still being created or disconnected can refuse restoration; let that operation finish, then retry `ocx stop`. The dashboard stop API returns HTTP 409 with `sharedTeardown: "refused"` when ownership cannot be confirmed and keeps the listener running. Signal cleanup also preserves configuration when ownership cannot be confirmed. To use Home from Codex again after restoration, start the Child with `ocx start` (or `ocx service start` for an installed service), then run `ocx sync --restart-codex`. Starting the runtime does not reapply a Child's Codex routing. Sync reapplies Codex settings when Codex is enabled for the saved connection, and the flag restarts Codex to load them; it does not recreate the Grok block. This is the explicit Link Child stop/start workflow; hub-client ownership and the separate restart, recycling, service-exit and sibling rules are unchanged. If **Child** says to pair this machine first, open this computer's configured literal-loopback HTTP dashboard, for example `http://127.0.0.1:`. The local pairing form appears only when pairing is missing and the dashboard and API use the same loopback origin. Copy the form's `ocx gui pair --origin "http://127.0.0.1:"` command, run it in a terminal on this computer, and paste the one-use code into the form. Use the exact origin shown in the form; a provider API key or admin token is not a pairing code. Missing pairing is separate from a configured-port mismatch. On a Hub dashboard opened through a non-loopback address, the browser may have no loopback session or may have a shared session that cannot manage machine links. When Remote Link reports that session or permission problem, choose **Connect this dashboard to the hub** in the Remote Link page to open the one-time pairing form. Run the displayed command on the Hub and enter its code. The page validates the new session's Remote Link access before it closes the form. The **Child** role also requires a standalone OpenCodex runtime running on its configured port, because the Child restarts on exactly that port. If the dashboard says OpenCodex is not running on its configured port, restart it there first. ## Link status - **Connected** means the SSH tunnel is ready and the Child can use the Home link. - **Reconnecting** means the tunnel is being retried. Requests can temporarily return `503` with `Retry-After` while the retry is in progress. On a Child that connected from its own dashboard, a request first waits up to 15 seconds for the tunnel to come back. - **Failed** means the link needs attention. Check SSH authentication, the confirmed host key, forwarding, or the timeout reason shown in the dashboard. The side that runs the SSH tunnel keeps retrying by itself, after sleep, an outage or a restart: Home for a Child it added, and the Child for a link it joined from its own dashboard. After a timeout or forwarding error the Home retries a Child it added every minute, so a laptop that slept reconnects within about a minute of waking; a Child that joined from its own dashboard retries after one minute and doubles the wait after each retry that fails again, up to five minutes. After an authentication error either side retries every five minutes. A changed host key is never retried. To try again at once, select **Retry** in the Home's dashboard or run `ocx link reconnect --link-id ` on the Home. It restarts that link's tunnel without restarting OpenCodex. A failed link does not silently switch to a local provider. The Child returns a retryable `503` without forwarding the link key or request when its tunnel is failed, stopped, or not supervised. It also keeps the key local when another process owns the tunnel port; free that port so the Child's SSH tunnel can bind it. On a Child, `GET /healthz` only says that OpenCodex is running. `GET /readyz` (and `ocx ready`) also says whether the link can carry a request right now: while it cannot, it answers `503` with `Retry-After: 1`, `"status": "pending"` and a `link` object such as `{ "state": "unavailable", "reason": "home_unreachable" }`, and while it can, `200` with `"status": "ready"` and `{ "state": "available" }`. The reasons are `tunnel_connecting`, `tunnel_failed`, `tunnel_idle`, `tunnel_unavailable`, `sidecar_invalid`, `home_unreachable`, `home_unauthorized`, `link_auth_failed`, `link_credential_unavailable`, `link_unverified` and `client_state_changed`. A Child that joined from its own dashboard answers from its tunnel. A Child that its Home added has no tunnel of its own to look at, so it asks the Home through the tunnel with the same credential-free check every relayed request starts with; it repeats that check at most once every five seconds, and answers `link_unverified` when the Home has not replied within half a second. The reason `client_state_changed` means the running Child's saved connection is missing or now names another owner or link. Its machine and link status endpoints also return `503` with this reason. Stop or restart the Child and check its connection before re-linking; this readiness result does not itself disconnect the relay. `ocx connect status` on a Child prints the same verdict as a `Link tunnel:` line under the local Codex CLI line: `up`, `down ()` with one of the reasons above, or `unknown` when no Child runtime is running (`runtime_not_running`), its answer could not be read (`runtime_unreadable`), or the Home check had not settled after one retry (`link_unverified`). `--json` carries it as `linkTunnel`. `Connection: connected` keeps meaning that this machine is linked; a Child whose laptop slept shows `Connection: connected` with `Link tunnel: down (home_unreachable)` until the Home reconnects the tunnel. If `ocx connect status --json` reports `disconnected` but a leftover service token prevents a new link, inspect the situation on the Child. After confirming that preserving the existing token files is intended, run `ocx disconnect --orphaned-token --yes`. It refuses a connected or pending client and any live client runtime or unverified runtime state. It also refuses a Hub, a non-loopback bind, a configured or matching admission secret, and an installed or unverified service: these can use the token for admission. On a Hub, manage the credential through the Hub service; otherwise inspect the bind, API keys, token environment and `ocx service status`, and remove or migrate a service only if intended before retrying. It moves the token and any rotation backup into a private directory under the OpenCodex config directory, prints the location, and does not infer that either token belonged to the link. On Windows, recovery requires hard-link support to preserve file identity while closing handles; if it reports an incomplete backup, inspect the reported directory and original files before retrying. If recovery was interrupted and a retry refuses with `unsafe token file` because another hard link exists, check the `orphaned-token-*` folders for a leftover `.service-api-token.identity` (or `.service-api-token.prev.identity`) file. Remove it only if it is a hard link of the current token file (the same file, not just the same name); keep any other pin, because it may be the only copy of an older token. Then inspect any remaining hard links to the token file before retrying. Stop OpenCodex to restore: move the saved files back only when the original names are absent. The command does not revoke a Home key or restore missing client configuration. To confirm which Home is serving a Child, compare the `x-opencodex-served-by` header on the Child's `GET /v1/models` with the `instanceId` in the Home's `GET /healthz`. The Child passes the Home's header through unchanged, so the two match while the link works. The value is an opaque id, never a hostname; see [Home identity and the served-by header](/reference/management-api/#home-identity-and-the-served-by-header). ## What a Child shows and manages A Child runs on the Home's providers, accounts and settings, so the Home is where you change them. - **Dashboard.** The Child's dashboard shows the Home's state read-only and says "Managed by Home over Remote Link". Dashboard and Providers read the Home through the link, and the usage strip in the header shows the Home's quota. The model list is not shown on a Child, because building it runs provider discovery on the Home; Codex on the Child already offers the Home's models. For the same reason, the delegation and sidecar model pickers on a Child list the native models and the Home's current choices, not the Home's full routed list. Saving a change there is refused with a message that names the Home; make the change in the Home's dashboard. The Child's browser never receives a Home credential. - **Codex settings.** `ocx sync` on the Child (and connecting it) applies the Codex settings OpenCodex manages on the Home: Fast mode, the multi-agent mode, the web search sidecar switch, and the native sub-agent defaults when the Home syncs them. `ocx sync` and `ocx status` list the values that came from the Home. The default reasoning effort and the effort levels shown in the Codex app's model picker are Codex app settings on each computer; OpenCodex does not manage them on any computer. When OpenCodex Desktop or a background service supervises the Child runtime, sync succeeds after applying settings and reports that the runtime restart is deferred. The settings take effect when that supervisor restarts the runtime: use **Quit** in the OpenCodex tray menu, then reopen OpenCodex, or run `ocx service restart` for a service. - **CLI.** Commands that only read, such as `ocx account list`, `ocx provider quota` and `ocx usage`, read the Home. They show what the Home has already observed: a read from a Child never makes the Home probe a provider, refresh a token or start a process, so quota and account details are as fresh as the Home's own activity keeps them. Reads that only exist to run provider discovery or a forced refresh, such as `ocx models live`, `ocx models preset show` and `ocx account list --quota`, are refused like changes. Commands that change configuration are refused with one message that names the Home and the link and says no request was sent. `ocx status` describes the Home and the link. `ocx provider list` and `ocx config get` still show this computer's own configuration, labelled as local and inactive while the computer is linked. `ocx config set`, `ocx config unset` and `ocx config import` still write locally, each with the same note, and `ocx provider add`, `remove` and `set-default` are refused because those providers are not used while linked. An import that would replace or remove an active client connection is refused. Disconnect that client first, or import a file that preserves its connection record exactly. These reads need an OpenCodex version with the Remote Link management relay on both the Home and the Child. An older Home answers with a message asking you to upgrade it. ## Remove a Child Select **Disconnect** for the Child and confirm the alias. The Home stops the tunnel, revokes that Child's link key, and removes the saved link record. On a lightweight (`ocx-child`) Child, disconnecting restores `~/.codex` and stops the relay with zero background processes while keeping `~/.cache/opencodex/bin/ocx-child` cached by default for fast reconnects; check **Also delete the planted lightweight agent binary from the remote computer** (or pass `--purge` to `ocx link revoke`) to delete the planted binary as well. Home runs that purge through the planted binary at `~/.cache/opencodex/bin/ocx-child` (`%LOCALAPPDATA%\opencodex\bin\ocx-child.exe` on Windows), never through another `ocx-child` on `PATH`. If that file is missing, or the Child does not confirm that it deleted the binary, Disconnect reports an error and keeps the link so you can retry, disconnect without the purge option, or remove the file yourself. `--purge` applies only to a lightweight Child: Home refuses it for a full OpenCodex install, and for a link saved before Home recorded the Child type it checks the Child first. If the Home cannot reach the Child to run its disconnect command, choose **Remove here only**. This removes the local tunnel, key, and record. Then log in to the Child and run: ```bash ocx disconnect # or ~/.cache/opencodex/bin/ocx-child detach on a lightweight Child ``` To disconnect a Child-initiated link, run `ocx disconnect` on the Child. It disconnects the client tunnel and revokes the link on Home over SSH. If Home revocation fails, it prints: `Home revoke failed; run ocx link revoke --link-id on the home.` On a full OpenCodex Child, `ocx disconnect` also restarts a client runtime that is still running, so the Child is serving standalone OpenCodex when the command returns. If that restart does not complete, the command exits non-zero and prints advice for the state it observed: `ocx restart` on the Child when the same runtime still serves the old connection; `ocx service restart` when a service that cannot be shown to relaunch the runtime refused the restart; and inspection with `ocx status` and `ocx connect status` when a new connection is in use, the client state cannot be read, or a different or unknown runtime answers. A client runtime whose connection was removed accepts `ocx restart` when its owner can relaunch it: the OpenCodex desktop app, or a background service that is enabled and running, starts the standalone replacement, so the same command works over SSH with nobody at the Child's screen. A service that cannot be shown to relaunch it (for example a unit started by hand while disabled) keeps the runtime and the command says to run `ocx service restart`. A runtime that carries only `OCX_SERVICE=1` (a service installed before 2.49, or a proxy started by `ocx claude`, `ocx codex` or `ocx opencode`) is not changed by this: `ocx disconnect` does not restart it, and `ocx restart` refuses while its service is viable, as before. The same holds for a client runtime started by an OpenCodex version before this change: `ocx disconnect` restarts a runtime only after it has been restarted once on the new version (an update's own restart, the desktop app restarting it, `ocx service restart`, or `ocx restart`). If `ocx disconnect` is refused before it has changed anything outside Codex, for example because the Codex configuration cannot be restored, it rolls itself back: the Child stays connected, its runtime still starts, and the message says the disconnect was rolled back. A Child left with a half-finished disconnect by an older release refuses to start its client runtime with `client startup refused: an ocx disconnect on this machine stopped part way`; run `ocx disconnect` again (with `--keep-catalog` if the first run used it) to finish it or roll it back. If an earlier run had already begun restoring Claude Desktop, the receipt is kept and the retry finishes the disconnect. Only one `ocx disconnect` runs at a time on a machine; a second one refuses with `client_disconnect_busy`. Codex adds its own lines to `~/.codex/config.toml`, such as trusted projects and notice flags. When it has done so after OpenCodex wrote its routing and no OpenCodex line remains, `ocx disconnect` leaves the file as it is and removes the Codex journal, whose snapshot would otherwise overwrite those lines. The one exception is your own root `web_search` line, if OpenCodex set it aside while routing: disconnect puts that line back. If an OpenCodex setting remains, disconnect keeps the journal and names it; remove each named key with its whole value and each named table with every line under it, then run `ocx disconnect` again. A refusal about the Claude Desktop remote profile store (`desktop_lifecycle_unsafe`) names the file or folder involved and the step that clears it. ## Troubleshooting When a step fails, the dashboard shows the reason and, when SSH reported one, a short sanitized hint from its last non-empty error line under the message. Remote-shell errors can appear there even when the remote shell emits non-UTF-8 text; OpenCodex removes terminal controls, link keys and URL queries and limits the hint's length. - **Could not connect to the SSH host**: the host must accept your SSH key without a password prompt; `ssh -o BatchMode=yes true` must succeed from a terminal. A `ProxyCommand` helper such as `cloudflared` must be installed in `/opt/homebrew/bin`, `/usr/local/bin`, `~/.bun/bin`, `~/.local/bin` or another directory on the PATH OpenCodex runs with. - **ocx was not found on the remote computer**: OpenCodex looks for `ocx` on the PATH of a non-interactive SSH session first, then in `~/.bun/bin`, `~/.local/bin`, `/opt/homebrew/bin` and `/usr/local/bin`. If it is installed elsewhere, add that directory to PATH in a file the remote shell reads for non-interactive sessions, such as `~/.zshenv` for zsh. - **OpenCodex on the remote computer is too old**: run `ocx update` on that computer. Remote Link needs 2.66.0 or later. - **The remote computer did not report an OpenCodex version**: `ocx --version` on that computer printed something else, for example the usage text of an unsupported Windows install. - **Requests fail with `home_unreachable` while the Home is down**: a Child never serves requests from its own providers. When the tunnel is down before a request is forwarded, the Child answers a retryable 503 and sends nothing to the Home. If the tunnel drops after forwarding started, the answer is also `home_unreachable`, but the Home may already have received the request. A failed link relay authentication answers `link_relay_auth_failed` instead: upgrade the Home and Child or re-link. Whether GOAT may move that device's work to its own sign-ins is the per-device local fallback permission on the Home's **Remote Link** page, or `ocx fallback local on --device `; see [Fallback off](/guides/fallback-off/). - **A Home link stays Failed with a forwarding error after the Home slept or changed networks**: run `ocx link status --json` on the Home. Its `tunnel` field shows the attempts since the link was last connected, `nextRetryAt`, and the last failure. A `lastFailure.cause` of `remote_forward_conflict` means the Child's SSH server still holds the tunnel port for the session that died with the Home's old connection, so every new tunnel is refused until it lets go. OpenCodex keeps retrying and connects on its own once the port is free; it never stops processes on the Child. The Child's SSH server notices a dead session only when it probes it. Without `ClientAliveInterval` in the Child's `sshd_config`, that can take as long as the operating system's TCP keepalive (two hours by default on macOS). Setting `ClientAliveInterval 15` and `ClientAliveCountMax 3` there makes it drop such a session within about a minute. ### Retryable 503 diagnostics A relay-generated `503` includes a server-generated `x-opencodex-link-request-id`. Use it to correlate a failure with local evidence under the Child's OpenCodex configuration home: `link/diagnostics.jsonl` and one rotated backup, `link/diagnostics.jsonl.1`. Each file is capped at 256 KiB, with private permissions (directory 0700 and files 0600 on POSIX; private ACLs on Windows). Records contain only fixed event/outcome/state categories, generated correlation IDs, numeric timing/generation/attempt metadata and timestamps, never secrets, request bodies, headers, URLs, account identifiers or raw errors. Each event/outcome pair is limited to one record per second; the next record includes a count of suppressed events, so an absent record does not prove an absent failure. A backward wall-clock adjustment starts a new rate-limit window, including when the writer queue is full, and preserves the suppression count. An asynchronous writer keeps at most 56 pending records, counts overflow as suppression, and yields while Windows ACL work runs. Logging is best effort and cannot change whether a request is admitted. Graceful signal shutdown and standalone recycling wait up to one second for queued diagnostics before exiting; stalled storage, forced termination and power loss can still lose records. To read them, run `ocx link diagnostics` on that computer, or open **Diagnostics** at the bottom of the `#remote` dashboard page there. Both show the newest records, oldest file first, with the time, event, outcome, state change, attempt and tunnel port of each. `--limit ` (1–1000, default 200) and `--since ` narrow the list, and `--json` prints the `ocx.link.diagnostics/1` document. The command reads the files directly: it needs no running proxy, creates and changes nothing, and prints the file path with **No Remote Link diagnostics recorded yet** when neither file exists. Both views check each line against the same fixed categories the writer uses: a line that does not match is never displayed and is counted as skipped (`skipped` in `--json`). A file that is a symlink, is not a regular file, or sits under a symlinked link directory is not read at all; the views say so, and `--json` reports it in `files` with state `refused`. The dashboard section uses the same authority as link status, and a Child's dashboard shows the Child's own records. `owner_lookup` records refused ownership checks; routine successful checks are silent. `owner_lookup.elapsedMs` measures the owner lookup itself. For `auth_timeout`, `relay_retry` and `relay_failure` report the actual transport-attempt duration in `elapsedMs` (normally about five seconds). A `relay_recovered` event with outcome `recovered` is emitted only after a retry receives an upstream response with `upstream.ok` (HTTP 2xx); it does not prove the streamed body completed. Rate limiting makes request-ID correlation sampled evidence, not a guaranteed trace of every failed request. The Home writes the same kind of file under its own configuration home for the tunnels it runs. Each change of a Home-initiated tunnel's state is a `tunnel_transition` record with `previousState`, `state`, the cause as `outcome` (`auth`, `hostkey`, `forward`, `remote_forward_conflict`, `timeout`, `network`, `unknown`, `spawn_failed` or `listener_unavailable`; `connected`, `spawned` or `stopped` when nothing failed), the `attempt` count since the link was last connected, the link's `tunnelPort` and, when a retry is scheduled, `retryAfterMs`. A retry of a failed link that fails again is recorded too, so an outage leaves one record per attempt. The same rate limit applies, across all links. Before sending data, OpenCodex allows one shared preflight retry for either an owner-lookup deadline or a specifically identified five-second authentication deadline. The retry obtains fresh owner proof where required and a fresh physical socket, then authenticates that socket before sending the link credential or body. It does not reuse cached owner approval, bypass security checks, restart SSH or OpenCodex, or replay a dispatched request body. The two-second owner-lookup and five-second authentication deadlines are unchanged for each attempt; other failures do not qualify for this preflight retry. This can recover these transient deadline paths and helps diagnose future failures. It does not establish the historical cause of [#7297](https://github.com/lidge-jun/opencodex/issues/7297). ### Capture SSH diagnostics When a tunnel keeps failing and the cause in `ocx link status --json` does not explain it, OpenCodex can record what SSH itself reports for a short time. Capture is off by default, and only someone working on the machine whose tunnel you want to inspect can turn it on: ```bash ocx link capture start # 15 minutes; --minutes accepts 1 to 60 ocx link capture status ocx link capture stop ``` Run it on the Home to capture the tunnels it runs to its Children, or on a Child to capture the tunnel that Child started to its Home. The command needs no running OpenCodex and no admin token. Nothing a Home syncs, and no dashboard or remote request, can start a capture. Running `start` again while a window is open moves its end time and keeps writing to the same file. While the window is open, the next SSH attempt of each tunnel runs with `LogLevel=DEBUG1` instead of `LogLevel=ERROR`. A tunnel that is already connected keeps its level until it reconnects; on the Home, `ocx link reconnect --link-id ` starts a new attempt right away. After `stop`, or when the window ends, nothing more is stored and the next attempt runs with `LogLevel=ERROR` again. Request admission is unchanged, and the extra lines SSH prints at the higher level are kept out of the failure cause that `ocx link status` reports. One exception: a line OpenCodex does not recognize still counts toward that cause, and SSH shows a server's login banner only at the higher level. During a window, a banner containing words such as "Permission denied" can therefore change the reported cause and, with it, the retry delay. Records go to `link/ssh-capture/ssh-.jsonl` under the OpenCodex configuration home, one JSON object per line with the time, the tunnel (`home` or `child`), the link ID, the SSH level, a fixed event name and the line text. Before anything is written, each line is matched against a list of known OpenSSH message shapes. Host names, addresses, user names, ports, file paths, keys and fingerprints, proxy commands, environment values and messages from the SSH server become placeholders such as `` and ``. Algorithm and authentication method names are kept only when they are standard OpenSSH names. A line that matches no known shape is stored only as `unrecognized` with a count, never its text. The link ID is OpenCodex's own random identifier; it is kept so you can tell tunnels apart. Each window writes one file of at most 1 MiB, and a `capture_truncated` record ends a full one. A file is deleted 72 hours after its last write, and at most four files are kept, so `start` deletes the oldest beyond that. Deletion runs when OpenCodex starts, when a tunnel starts, on every `ocx link capture` command, and in a running OpenCodex as soon as a file's 72 hours pass, including files an earlier OpenCodex process wrote. A file whose 72 hours pass while OpenCodex is not running is deleted at the next start or capture command. `stop` leaves the files in place so you can share them; delete them yourself once you no longer need them. The directory is private to your user (directory 0700 and files 0600 on macOS and Linux, a private ACL on Windows), and OpenCodex never writes or reads through a symbolic link there. ## Security The Child uses the Home computer's providers and provider credentials through the link. The Home creates a separate link key for each Child; removing the link revokes that key. On the Child, the key stays inside OpenCodex: credentials that Codex or Claude Code send there are not forwarded to the Home, including Bearer, Azure `api-key`, Anthropic-compatible `x-api-key`, and Google `x-goog-api-key` forms. Any program on the Child that reaches `127.0.0.1:` uses the Home without a key, the same local trust a standalone install gives. Web pages from other sites are refused. Compare the host fingerprint before confirmation so a wrong machine or changed host key is not accepted by mistake. Dashboard sessions issued from a Tailscale identity cannot manage machine links. ## CLI reference ```text ocx link probe [--json] ocx link add [--expect-fingerprint SHA256:…] [--json] ocx link port [--json] ocx link issue --alias --tunnel-port [--json] ocx link status [--json] ocx link diagnostics [--limit ] [--since ] [--json] ocx link revoke --link-id [--json] ocx link reconnect --link-id [--json] ocx link check (--link-id | --all) [--no-wait] [--json] ocx link update (--link-id | --all) --yes [--no-wait] [--json] ocx link sync (--link-id | --all) [--no-wait] [--json] ocx link sync --link-id (--restart-app-server-only | --restart-codex --yes) [--no-wait] [--json] ocx link restart-codex --link-id [--scope app-server|codex] --yes [--no-wait] [--json] ocx link capture start [--minutes <1-60>] [--json] ocx link capture stop [--json] ocx link capture status [--json] ocx link policy [--updates off|notify|auto-when-idle] [--catalog-fanout auto|manual] [--child-codex-restart never|app-server-when-idle] [--idle-after-minutes <1-60>] [--json] ``` `ocx link status` needs the local admin token on a listener that only this machine can reach. When the public listener is bound to a non-loopback address, for example a Hub on `0.0.0.0`, no TCP listener qualifies: the hub management listener is reached through Tailscale Serve, so it refuses the raw admin token for link routes. On macOS and Linux the proxy therefore also opens a private Unix socket, `/run/control-.sock`, in a directory only your user can enter, and `ocx link status` uses it automatically. The socket answers only `GET /api/link/status` and still requires the admin token. Windows and runtimes older than this change keep the TCP path, where the read returns `403` on such a Hub. ## Related guides - [Remote Hub Deployment](/guides/remote-hub/) - [Remote Workspace](/guides/remote-workspace/) ### Relay authentication compatibility Update both the Home and Child when upgrading to connection-bound relay authentication. Before sending a relayed request's link credential or body, the Child verifies the Home on the same connection it will use for that request. A closed connection is not silently replaced. A Home without this protocol causes a retryable authentication error; upgrade the Home and Child, and re-link when the stored link is no longer recognized. There is no insecure fallback switch. An unexpired pending API-key rotation remains valid until it expires or the rotation is committed or aborted. Removing the final Home link drains pending authenticated relay requests before releasing its listener. Stopping the process still cancels active connections. This does not change which caller credentials are stripped or which routes can be relayed, and it does not replace SSH's host-key verification. ### Home and Child versions On the Home, `ocx link status --json` reports the Home's own `local` version, relay authentication version and features, and gives every link a `peer` block. Nothing is read over SSH: the Home learns about a Child from the relay authentication check the Child sends before each relayed request. From 2.91.0 that check also carries the Child's version and feature list. The Home keeps what it learned in memory, so after a Home restart a link reads `unknown` until its Child relays a request again. | Part of the link | First release | |---|---| | `relay-auth/2`, connection-bound relay authentication | 2.69.0 | | `link-management-relay`, read-only Home management on the Child | 2.84.0 | | `link-peer-report/1`, the Child reports its version and features | 2.91.0 | `peer.compatibility` is one of: - `compatible`: the relay authentication versions match, and the Home serves every feature the Child reports. A Child older than 2.91.0 reports no version; `peer.version` is then `null` and `peer.versionRange` gives the range its relay authentication implies, for example `{ "min": "2.69.0", "below": "2.91.0" }`. - `degraded`: the Child reports a feature this Home does not serve. Upgrade the Home. - `incompatible`: the Child asked for a relay authentication version this Home does not speak, so its relayed requests are refused. `peer.reasons` names the side to upgrade. - `unknown`: the Child has not relayed a request since the Home started, or it sent a version or feature list the Home could not read, so its features cannot be checked. The version a Child reports is its own claim, bound to its link key. OpenCodex uses it for display and never for admission. Older `ocx` installations keep receiving the previous status document; only a 2.91.0 or newer CLI asks for these fields. ### Child install, catalog and activity `ocx link status --json` on the Home also gives every link a `child` block. An older Home omits it, and an older CLI never asks for it. - `runningVersion` is the version the Child reported over the link. `installedVersion`, `installKind` and `update` (`route`, `reason`, `observedAt`) come from the last `ocx link check` and stay `null` until one ran. `versionSkew` compares the installed version, or the running one, with the Home's: `same`, `behind`, `ahead` or `unknown`. It is always `unknown` for an `ocx-child` Child. - `catalog.state` is `current` when the catalog the Child last downloaded from the Home is the Home's current one, `behind` when the Home's catalog changed since, and `unknown` before the first download. The Home compares the catalog's content, not file times, so restoring an older catalog file also reads `behind`. A download proves only that the Child fetched the catalog, not that it applied it; the check `ocx link add` runs at enrollment counts as a download. - `activity.idle` is `true` when no relayed model request has run on that link for the policy's idle period (10 minutes by default; see `ocx link policy --idle-after-minutes` below). `inFlight` counts the requests running now. Token counting, catalog and readiness reads do not count, and a request from a Child older than connection-bound relay authentication counts against every link. - `activeJob` names an operation running on that link, and `lastOperation` the last one that finished. `codex` is `null` for now. `ocx link check --link-id ` (repeatable) or `ocx link check --all` asks Children for their install facts. For each Home-initiated Child the Home runs the Child's `ocx update --plan` over the link's SSH connection, which computes an update plan and changes nothing; each call has a 60-second budget and three run at a time. A Child older than 2.97.0 does not have that command: the item reads `owner-action` with reason `legacy-ocx`, and the Home learns the version from one more read-only SSH call on macOS and Linux, or from the version the Child reported over the link on Windows. On an npm or pnpm Child without a recorded serving install, the command runs through the `ocx` launcher, so the launcher's usual startup bookkeeping runs first, as it does for every `ocx sync` the Home sends there. Links a Child started from its own dashboard and `ocx-child` Children are reported without SSH. The command waits for the job and prints it as JSON (`--no-wait` prints it at once); it exits 0 when every Child answered, 6 when a Child is busy, offline, too old or not checkable, 1 on a failure, 4 for an unknown link or a Home too old for this command, and 5 when another operation is running on that link or the Home has no links. ### Operating Children from the Home From the Home you can also sync a Home-initiated Child, restart Codex on it, and update its OpenCodex. Each command starts one job on the Home, runs over the link's SSH connection, waits for the job and prints it as JSON (`--no-wait` prints it at once). - `ocx link sync` runs the same pinned `ocx sync --expect-link` that `ocx sync` sends to Children. It needs OpenCodex 2.93.0 or later on the Child. - `ocx link restart-codex --link-id --yes` syncs first and then restarts the Child's Codex app-servers, so a running Codex loads the current catalog. `--scope codex` also quits and relaunches the Codex desktop app, which needs a signed-in desktop session on the Child. `ocx link sync --link-id ` with `--restart-app-server-only` or `--restart-codex --yes` does the same. A restart ends the Codex turns running on that Child, so it names exactly one link. It needs 2.93.0 or later. - `ocx link update (--link-id | --all) --yes` brings the Child's OpenCodex to this Home's version, never to another one, through the Child's own updater. The Home first confirms that the Child still belongs to this link, then asks the Child for an update plan, applies it with a deadline counted on the Child's own clock, and syncs again after a successful, failed or rolled-back apply. An apply whose result is unknown is neither retried nor resynced; check that Child with `ocx link check`. npm and pnpm installs are updated through their Node launcher and Bun installs through Bun; `ocx link check` on an npm or pnpm Child therefore also runs `node --version` there, read-only. Updates need 2.97.0 or later on the Child; an older Child receives no update command and reports `owner-action` (`legacy-ocx`). Updates run one Child at a time and restart the Child's OpenCodex, which ends the requests the Home is relaying for it, so `--yes` is required. The Home does not restart Codex on a Child while it is relaying a model request for that Child. `ocx sync --restart-children` (or `--restart-children=codex --yes`) applies the same rule when a Home sync fans out, to every Child or to those named with `--child `: each Child is synced, and only its restart is skipped (`restart-skipped:link-active`) while a relayed request is in flight. If a request is already in flight when `ocx link restart-codex` starts, nothing is sent and the item is `busy` (`restart-skipped:link-active`). If one starts during the sync that precedes the restart, or the restart was requested with `ocx link sync`, the sync still runs and only the restart is skipped (`restart-skipped:link-active`). This protection is best-effort: a request that starts between that check and the restart can still be ended, so pause new requests on that Child when its turns must survive. A Child older than 2.69 is seen as busy only once its response starts for a non-streaming request, so a restart can still end such a request. Windows Children support `sync`; restarts and updates from the Home are refused there (`windows-restart`, `windows-apply`). `ocx-child` Children report restarts and updates as unsupported. One operation runs on a link at a time. Removing a link while an operation runs on it is refused with `409 link_operation_in_progress`; remove it once the operation has finished. While a link is being removed, a new operation that names only that link is refused with the same 409, and a job that names several links reports that link's item as `busy` (`link-operation-in-progress`) while its other links still run. The commands exit 0 when every Child succeeded or was already up to date (with `--no-wait`, 0 only means the job was accepted; it may still be running. The printed job carries a `jobId`; read the result with `GET /api/link/children/jobs/` on the Home, using the admin token on loopback. Finished jobs stay readable for one hour, at most 32 of them, and are lost when the Home restarts, so read the result promptly), 6 when a Child needs attention (busy, offline, refused, an owner action, unsupported, a skipped restart, or an update that was kept but whose follow-up sync failed; run `ocx link sync` for that link), 1 on a failure or an unconfirmed result (check the Child again with `ocx link check` before retrying), 4 for an unknown link or a Home without these commands, and 5 when another operation is running on that link, the only link named was started from the Child, the Home has no links, or the command ran on a Child; the error message names which. The CLI needs a Home bound to loopback; on any other Home these commands are refused (exit 1), and the dashboard has no controls for them. Do not link a machine you do not own: the Home runs commands there with your SSH key. ### Automatic catalog fan-out and the Child policy Catalog fan-out is on by default. When the Home's catalog changes (the catalog auto-refresh reports a change, or a check every 5 minutes notices a dashboard or management edit), the Home waits 60 seconds for edits to settle (at most 5 minutes after the first change). Then, with no action from you, it opens a background SSH session to each eligible Child and runs the same pinned `ocx sync --expect-link` that `ocx link sync` runs, without a Codex restart unless you enable one (`childCodexRestart`, below). A Child is eligible when the Home started its link, its tunnel is connected, no other operation runs on it, and the last sync this Home ran there predates the change; a Child that only downloaded the catalog still counts as behind. A Child that is relaying model requests is synced only when its Codex settings already match the Home's and its OpenCodex client runtime has restarted since those settings were applied, because otherwise the sync restarts that runtime; otherwise it is synced once it is idle. A failed sync is retried after 5 minutes, doubling up to 6 hours, and a Child older than 2.93.0 is not retried until it reports another version. These jobs look like any other job, with `trigger: "auto"`. A Home with no links runs none of this. The fan-out fixes the Child's catalog file. A Codex app-server already running on the Child keeps the list it loaded until it restarts: use `ocx link restart-codex --link-id --yes` or `ocx sync --restart-children`, or let the Home restart app-servers with `ocx link policy --child-codex-restart app-server-when-idle`, which it does only on a Child that is still idle when its sync runs. That setting applies to the next automatic sync; a Child whose catalog is already current gets no sync and so no restart, so restart that Child yourself. The policy lives in `/link/child-policy.json` on the Home, never in `config.json`: | Setting | Default | Values | |---|---|---| | `catalogFanout` | `auto` | `auto`, `manual` | | `childCodexRestart` | `never` | `never`, `app-server-when-idle` | | `updates` | `auto-when-idle` | `off`, `notify`, `auto-when-idle` | | `idleAfterMs` | `600000` (10 minutes) | 1 to 60 minutes | This release stores `updates` but starts no automatic update yet; `notify` and `auto-when-idle` take effect when the automatic update sweep ships. `updates` defaults to keeping idle Children on the Home's version automatically. Automatic updates are not active in this release; the setting is stored now, so an opt-out with `ocx link policy --updates notify` or `--updates off` holds when they arrive. `idleAfterMs` is how long a Child must go without a relayed model request to count as idle; it also decides `activity.idle` in `ocx link status`. ```text ocx link policy # read the policy ocx link policy --catalog-fanout manual # turn automatic catalog fan-out off ocx link policy --child-codex-restart app-server-when-idle ocx link policy --updates off --idle-after-minutes 20 --json ``` `GET` and `PUT /api/link/children/policy` read and change the same settings (the admin token on loopback, or a dashboard session); a `PUT` sends only the settings it changes. If the file is damaged or holds an unknown value, automatic catalog fan-out and updates are off, and `ocx link policy` says so. Saving clears the warning but keeps every setting you do not name off, so turn them back on explicitly, for example `ocx link policy --catalog-fanout auto --updates auto-when-idle`. The Home never rewrites the file on its own. ### Standalone pairing authorization The existing `ocx gui pair --origin ` command works while OpenCodex runs as a background service. Run it under the account that owns the service's private OpenCodex home, then enter the returned one-use code in the local dashboard. No server terminal, additional password, Windows Hello or hardware key is required. The CLI and server must use the same current version and configuration home; restart an older server after updating. Mixed-version Hub invitations also require updating the CLI and server together. Pairing refuses a symlinked configuration home or intent directory; use the actual protected directory as `OPENCODEX_HOME`. Internally, the CLI creates a short-lived authorization commitment and attempts to remove it after use or failure. Reading runtime information alone does not authorize pairing, and the verifier is not stored on disk. This is a private-home write-access boundary, not biometric or human-presence authentication: a program with full access to that user's files is still trusted. Existing Hub invitation authorization is unchanged. If a pairing request fails, run a fresh command after resolving the reported problem. SSH Remote Link on Windows uses the separate PowerShell execution path described above. If a process is interrupted during pairing, rerun the command to obtain a new authorization. Do not manually remove pairing state while the service or a pairing command is running. On Windows, pairing fails closed if the intent folder or record is not private to the serving account, or its owner and permissions cannot be verified. Both entries must be owned by that account; Administrators ownership is accepted only when Administrators is the serving token's default owner and its administrator role is enabled. The permission list must be protected from inheritance and contain an ordinary Full Control allow entry for the serving account. Besides that, only plain allow entries for SYSTEM and the Administrators group are accepted, because SYSTEM and an elevated administrator can already take ownership of any file; a non-elevated administrator gains nothing from that entry. Child inheritance is allowed on the folder only. This is the list as Windows reports it: compatible duplicate entries for the same account count as one, and entries that grant nothing are ignored. The check leaves refused records in place and does not repair their permissions or change ownership. Restore a private configuration home, then run a fresh pairing command. The check covers the intent folder and record. It does not protect against replacement of ancestor folders, deletion rights on a parent folder, privileged restore operations or access through handles opened before permissions became private. A request with a matching commitment can block the server during a verification subprocess bounded at five seconds, even if its folder later fails the permission check. Executable lookup and process launch setup precede that timeout. CLI cleanup may perform one more verification. The five-second subprocess budget leaves room within the command's ten-second request timeout, but slow verification can still refuse pairing. ## Add a Child from the terminal On the Home, run `ocx link probe child-a --json` to inspect the offered SSH fingerprint, key type and OpenCodex version. Verify the fingerprint through a trusted source. Then run `ocx link add child-a --expect-fingerprint SHA256:… --json`. The command probes again and refuses a mismatch before storing trust or issuing a key. Without the fingerprint flag, it requires an interactive terminal and an explicit `yes` after showing the fingerprint. It never prints the link key. Home-initiated connections now restart the Child after saving its connection, including capable older Child installations. The Child must already serve standalone OpenCodex on its configured port and expose bound restart support. POSIX Children need curl; Windows uses a bounded loopback HTTP request through .NET. Update or disconnect a Child that fails this preflight. On macOS and Linux Children, Home first runs the OpenCodex installation that most recently started serving the Child, including an npm install under a custom prefix, even when another installed copy is newer. An older desktop app or PATH copy no longer answers for it. If that installation is gone or cannot report its version, Desktop and system OpenCodex installations take precedence over generic PATH shims; broken candidates are skipped. Windows x64 Children use encoded PowerShell commands without consuming key stdin. The preflight reads `ocx config show --json` under the runner's 4 MiB capture limit, so a Child with a large saved configuration still accepts a link; output past that limit answers `child_preflight_output_limit` and names the command to trim. When a phase fails, `ocx link add` prints the phase together with the Child's own reason, for example an invalid saved configuration or the failing command's last error line. That reason is shortened and has OpenCodex keys and URL query strings removed. ### Ephemeral Child machines (`ocx-child`) For disposable SSH devboxes, containers and lightweight cloud VMs where installing the full Bun OpenCodex runtime is unnecessary, Home can stream a SHA-256-verified standalone `ocx-child` binary (no Node or Bun required on the Child) over SSH `stdin` into `~/.cache/opencodex/bin/ocx-child` (`%LOCALAPPDATA%\opencodex\bin\ocx-child.exe` on Windows) when you choose **Plant lightweight agent & connect** in `#remote` or run `ocx link add --plant --expect-fingerprint SHA256:…` (an interactive `ocx link add ` asks once before planting). The Child downloads nothing, so it needs no outbound internet access, but a POSIX Child must have `sha256sum`, `shasum`, or `openssl`: the binary is checked against its SHA-256 before it is installed or run, and without one of those tools the plant is refused (`hash_tool_unavailable`). You can also pre-place `ocx-child` in `~/.local/bin/ocx-child`, `~/.cache/opencodex/bin/ocx-child`, or `PATH`. Before enrollment, Home probes every full `ocx` candidate first and falls back to `ocx-child` when no runnable full `ocx` is present; once `$OPENCODEX_HOME/child/state.json` exists, remote commands keep targeting `ocx-child` until it detaches. Home plants only a build it can verify, and it does not download or build one. The npm package and the desktop app ship no `ocx-child` builds, so on a packaged install the probe reports the bare host as not plantable (`plantable: false`, `plantUnavailableReason: "artifact_not_found"`) until you provide one. Home looks in these places, in order, for the Child's target (`darwin-arm64`, `darwin-x64`, `linux-x64`, `linux-arm64`, or `windows-x64`): 1. The directory named by `OPENCODEX_CHILD_BIN_DIR` on Home: `ocx-child-` (`ocx-child-windows-x64.exe` for Windows) or `/ocx-child`, plus plain `ocx-child` when the Child has the Home's own target. The file must be listed in a `SHA256SUMS` file in the same directory. 2. A `native-bin/` directory next to the OpenCodex sources or the running executable, with the same `SHA256SUMS` rule. No OpenCodex package provides this directory today. 3. In a source checkout only, `native/ocx-child/target/release/ocx-child` when the Child has the Home's own target. 4. `$OPENCODEX_HOME/cache/ocx-child//ocx-child-` with a `SHA256SUMS` file beside it. Nothing fills this cache automatically; copy builds there yourself. To prepare a build, run `cargo build --release --locked --manifest-path native/ocx-child/Cargo.toml` in a source checkout (cross-compiling for other targets), copy the result to one of those directories under the `ocx-child-` name, and write its `SHA256SUMS` line with `sha256sum ocx-child- >> SHA256SUMS` (or `shasum -a 256`). The files must not be symlinks or group- or world-writable. Unlike a full `ocx` Child, a clean detached `ocx-child` host needs no pre-running background daemon before `ocx link add`: Home checks target-port availability and `~/.codex/config.toml` ownership before committing (`409 child_port_busy` if port `10100` is held by another process, or `409 child_external_provider_conflict` if another model provider or custom base URL already owns `config.toml`, leaving Child files untouched and rolling back any newly planted binary), pipes the link credential over SSH stdin, starts `ocx-child serve`, and confirms an authenticated `/v1/catalog` relay. Connected rows in `#remote` display a **Lightweight** / **Full install** badge and a **Home settings synced** / **Sync needed** parity badge. Running `ocx link revoke --link-id ` keeps the cached binary for fast re-attach, whereas `ocx link revoke --link-id --purge` (or `ocx-child detach --purge` on the Child) also deletes the planted binary from the managed cache directory. Keep `OPENCODEX_HOME` and `CODEX_HOME` pointed at the same directories throughout the connection. If detach reports a restore or shutdown error, keep the Child's recovery files and resolve the error before retrying. ### Windows computers A Windows x64 Child needs OpenSSH Server, Windows PowerShell and either standalone `ocx.exe` or lightweight `ocx-child.exe`. Home recognizes Windows during the probe, records it with the link, and runs every Child command through PowerShell over the same pinned SSH connection, whether the SSH default shell is cmd or PowerShell. The link key still travels only on standard input. Closing the SSH connection ends the selected remote Child command. If Windows PowerShell on the Child cannot load the helper that contains the selected executable in a kill-on-close Job, for example under Constrained Language Mode, the command is refused and nothing runs. If Home cannot tell which platform the Child runs, it stops before issuing a link key. Run `ocx link cleanup` on Home to inspect recorded Child platforms without changing them. Current Home releases disconnect Windows Children through PowerShell. If a remote disconnect fails, resolve the reported shell or installation error and retry; **Remove here only** remains an explicit recovery action that can leave the Child needing a local disconnect. Restart waits for running turns and may take several minutes. Success requires the restarted Child to serve the client role and relay a fresh complete catalog to this Home. SSH setup has a bounded allowance derived from measured latency; relay verification starts after restart with its own 15-second window. Errors name the failing phase and end with the error code. If an error reports residual Child configuration, run `ocx disconnect` and `ocx restart` on that Child before retrying. Home cleanup alone does not restore Child configuration. A CLI timeout reports an uncertain outcome; inspect Home `ocx link status` and Child `ocx connect status` first. A `Child restart preflight` failure (`child_restart_unsupported`) names what the Child is and the Child command that fixes it. Typical causes are a Child still linked to a Home (run `ocx disconnect`, then `ocx restart`), a Child still serving a client runtime (`ocx restart`), a Hub (`ocx hub disable`), a runtime without restart support (update OpenCodex), and a runtime answering on a port other than its configured one (`ocx restart`). On a Home-initiated Child, `ocx link status` reports role `child`, link identity and tunnel port from its saved connection even though its Home-side link store is empty. Locally unobserved tunnel state is `idle`. `ocx status` calls the peer a Remote Link Home. When Home runs in Hub mode, status reads its supported projection through the local Child relay. A standalone Home reports that it does not publish Hub state. Child-initiated tunnel identity comes from its valid sidecar; unreadable evidence does not claim a direction.