Remote Codex in NovaScale
How NovaScale uses the Codex app-server over a private Tailnet to bring remote Codex workflows into an iOS app.
Background
Sometimes I need to handle server operations from my phone after receiving alerts from my self-hosted Grafana instances. I may need to inspect a service, research an error, or write a quick script. Using an AI chat app helps, but I kept wondering whether NovaScale could offer something closer to an agent interface, not just a chat interface.
The idea stayed in my head for a while, but I did not have a clear shape for it.
At the end of April, I decided to unsubscribe from Anthropic’s Claude because Codex had started to cover most of my development workflow. In particular, I liked that Codex did not interrupt me in the middle of a session because I hit a five-hour limit.
Then I saw a post on X mentioning a Codex feature called app-server. That sent me into research mode. I read the official app-server documentation and also looked through the open-source Codex CLI code. I liked that the implementation is open source; it makes this kind of integration much easier to understand.
The key details I learned were:
codex app-serveris the interface Codex uses to power rich clients, including deep integrations with authentication, conversation history, approvals, and streamed agent events.- It supports JSON-RPC 2.0-style bidirectional communication. For WebSocket transport, each JSON-RPC message is sent as one WebSocket text frame.
- The TCP WebSocket transport is documented as experimental and unsupported, so it should be treated as an integration point that may change.
- The CLI can generate TypeScript or JSON Schema definitions for the app-server API, and those generated schemas match the Codex version used to generate them.
- Non-loopback WebSocket listeners should be protected with WebSocket authentication before being exposed remotely.
Because a WebSocket listener can bind to a non-loopback address, I realized I could bind it to a Tailscale address on the host. I already knew JSON-RPC from my older Open vSwitch work, and while I had not used WebSocket heavily before, the transport was straightforward enough given my background in L3/L4 networking.
I quickly built a prototype in NovaScale, and the app-server path worked well enough to become part of the 1.5.0 release. I also found that it could support small file uploads. That opened an unexpected workflow: if I see an interesting plant or flower, I can take a photo, send it from my phone to the Codex app-server on my Mac, and ask Codex to describe it. The image lands in the same workspace created for that task.
There is a practical limit here. The WebSocket path uses text frames, so binary files need to be base64 encoded. That is fine for small images or documents, but it is not a replacement for large file transfer.
OpenAI’s own remote Codex
OpenAI now has its own remote Codex story. The official Remote Connections documentation describes using Codex from another device, including the ChatGPT mobile app controlling a connected Mac or Windows host, and the Codex App connecting to projects on an SSH host.
That product direction makes sense. The connected host provides the files, credentials, local tools, plugins, MCP servers, browser access, and security settings, while the phone sends prompts, approvals, and follow-up messages. OpenAI also documents a secure relay layer for trusted devices, which avoids exposing app-server transports directly to the public internet.
NovaScale is not trying to replace that. My goal is narrower: a Tailscale-native workflow for users who already keep their machines inside a private Tailnet and want to reach Codex from the same iOS app they use for SSH, monitoring, and internal web access.
I still marked the NovaScale Codex integration as experimental. It is useful today, but app-server’s WebSocket transport is still documented as experimental, and I want users to be able to remove the tab if they do not need it. Tab space is precious on iOS.
How it works under the hood
Codex app-server
During the first prototype, I could start a listener with a terminal command like this:
codex app-server --listen ws://100.64.x.y:14500
That was enough for NovaScale to connect to a host inside my Tailnet and talk to the Codex app-server.
Later, after updating Codex CLI, I saw a stricter startup error when trying to use a non-loopback WebSocket listener without authentication:
Error: refusing to start non-loopback websocket listener 100.64.x.y:14500 without auth; configure `--ws-auth capability-token` or `--ws-auth signed-bearer-token`
This is the right direction. Even though Tailscale already protects my Tailnet with WireGuard under the hood, a non-loopback service still deserves its own authentication layer. It also pushed me to move away from ad-hoc terminal commands before shipping NovaScale 1.5.0.
The official app-server docs describe two WebSocket auth modes:
capability-token, backed by a token file or SHA-256 verifiersigned-bearer-token, backed by a shared secret and JWT-style bearer tokens
For NovaScale, the practical shape is:
- Install Codex CLI on the host.
- Start
codex app-serveras a service on a Tailnet address. - Require WebSocket authentication for the listener.
- Import the host URL and token into NovaScale.
- Use NovaScale to create or resume threads and stream turn events.
It is now quite easy to ask an agent to write a macOS LaunchAgent or Linux systemd unit, but I wanted a repeatable path. I wrapped the setup into an automation script:
NovaScale Codex bootstrap script on GitHub
When the script finishes, it tries to render a QR code if a QR encoder is available. It also prints a NovaScale URI that can be copied and pasted into the app.
I also added this workflow directly to NovaScale. If you already have an SSH configuration for a host in your Tailnet, you can pick that host from the app. NovaScale connects over SSH, installs the service, and imports the resulting Codex host configuration.
App integration
The app side is simple because NovaScale already has built-in Tailscale connectivity.
NovaScale talks directly over the private Tailnet and calls the APIs exposed by codex app-server. The app sends JSON-RPC requests such as initialization, thread operations, and turn starts. The server streams events back so the UI can show messages, tool progress, approval prompts, and completion state.
The harder part is not the transport. The hard part is making the chat and agent experience feel fast, immersive, and native on iOS.
NovaScale 1.5.0 is only the beginning. I will keep improving the interaction model as I use it more.
What’s missing and what’s not ideal
Everything has tradeoffs. NovaScale currently uses a dedicated app-server service for each configured host. That service can load Codex state, list or resume threads, start turns, stream events, and surface approval prompts inside the app.
The limitation is that this is not the same runtime as a separate Codex CLI or Codex Desktop process the user may also have open. If an approval is pending inside another process, NovaScale cannot approve that other process’s in-memory state. NovaScale can show and manage approvals for the turns it starts through its own app-server connection, but separate runtimes are still separate runtimes.
The good news is that threads are still resumable. From NovaScale, I can start a new turn on the same thread, ask for the current status, and continue the work.
Another limitation is that the local Codex CLI does not automatically become a live mirror of changes made through NovaScale’s associated app-server. When I return to the host screen, I usually run codex resume to pick up the thread again.
This is not ideal. I want to explore whether NovaScale can eventually share a single app-server runtime with other clients, or use a different architecture that aligns more closely with Codex’s documented remote mode.
Pending work
NovaScale currently uses in-app notifications and local iOS notifications when a turn needs attention, such as an approval prompt.
That works, but it is not enough for long-running background work. iOS can suspend or terminate NovaScale while work continues on the remote Codex host.
A more reliable approach is to use Apple Push Notification service. I see two possible designs:
- Run a push notification service and let users configure Codex hooks, or durable instructions in
AGENTS.md, to notify that service when important events happen. - Replace the vanilla app-server service with an open-source daemon that wraps app-server, proxies traffic between NovaScale and Codex, watches turn progress, and sends push notifications when user attention is needed.
I currently prefer the second option. It keeps the host-side integration explicit, and the daemon can still talk to NovaScale directly over the private Tailnet.
Closing thoughts
There are now many ways to use Codex remotely. NovaScale can be a useful alternative for users who want a Tailscale-native workflow inside an iOS operations app.
I will keep working on it. If you have suggestions or find bugs, feel free to email us or leave a comment on the App Store. I read that feedback carefully and use it to guide the next round of work.