mech.app

The mech.app newsletter

Agentic AI, minus the noise.

Get practical field notes on AI agents, automation, developer tools and security delivered to your inbox.

No spam. Unsubscribe anytime.

Dev Tools

T3 Code's Agent Harness Control Surface: How a Mobile-First UI Manages Codex, Claude, Cursor, and Grok Build Across Machines

Architecture of T3 Code's multi-provider agent control plane: CLI subprocess orchestration, session persistence, and mobile-to-localhost tunneling.

Source: github.com
T3 Code's Agent Harness Control Surface: How a Mobile-First UI Manages Codex, Claude, Cursor, and Grok Build Across Machines

T3 Code sits one layer above the agent runtime. It does not implement a coding agent. It orchestrates five of them: Codex CLI, Claude Code, Cursor CLI, Grok Build CLI, and OpenCode. The project hit 17,402 stars and trending #2 for TypeScript because it solves a specific pain: developers want to control coding agents from their phone or a lightweight web UI without being locked into a single vendor’s desktop app.

This is a rare open-source example of a harness control surface. The backend spawns and manages CLI subprocesses, maintains authentication sessions across restarts, and exposes a unified API to Electron, web, and native mobile clients. The mobile apps (iOS App Store, Android Play Store) connect to a local backend running on your laptop, even when you are on a different network.

Architecture: Subprocess Orchestration and Session Persistence

T3 Code’s backend is a Node.js server (22.16+, 23.11+, or 24.10+) that wraps five heterogeneous CLI tools. Each provider ships its own authentication flow:

  • Codex: codex login
  • Claude Code: claude auth login
  • Cursor: cursor agent login
  • Grok Build: grok login
  • OpenCode: opencode auth login

The backend does not re-implement these flows. It assumes you have already authenticated locally. When you launch T3 Code, the backend checks for valid session tokens in each provider’s credential store (typically ~/.config/<provider>/ or OS keychain). If a session is missing or expired, the UI surfaces a warning and blocks agent commands for that provider.

Subprocess Lifecycle

Each agent command spawns a new subprocess. T3 Code does not maintain long-lived connections to the CLI tools. Instead:

  1. The mobile or web client sends a command (e.g., “refactor this function”) to the backend over HTTP or WebSocket.
  2. The backend selects the appropriate CLI binary based on the user’s provider choice.
  3. It spawns the subprocess with child_process.spawn, pipes stdin/stdout, and streams the agent’s response back to the client.
  4. When the agent finishes or the user cancels, the backend kills the subprocess.

This design avoids state drift. Each command is a fresh invocation. The downside: startup latency for each command. The upside: no need to manage long-lived agent sessions or handle partial state corruption.

Session Persistence Across Restarts

The backend does not store credentials. It relies on each provider’s native credential store. When the backend restarts, it re-reads the credential files. If a provider’s CLI tool rotates tokens or invalidates sessions, the backend will fail the next command and prompt re-authentication.

This is a deliberate trade-off. T3 Code avoids becoming a credential manager. It delegates that complexity to the providers. The risk: if a provider changes its credential format or location, the backend breaks until someone updates the wrapper code.

Mobile-to-Localhost Tunneling

The mobile apps connect to a local backend running on your laptop. This requires solving two problems:

  1. Discovery: How does the mobile app find the backend when it is on a different network?
  2. Connection stability: What happens when the agent is mid-task and the connection drops?

Discovery: Local Network and Tunneling

T3 Code supports two connection modes:

  • Local network: The backend advertises itself via mDNS (Bonjour on macOS, Avahi on Linux). The mobile app scans for _t3code._tcp services and connects directly over HTTP. This works when your phone and laptop are on the same Wi-Fi network.
  • Remote tunnel: The backend can optionally expose itself through a tunnel service (e.g., ngrok, Cloudflare Tunnel). The mobile app connects to a public URL that forwards to localhost. This works from anywhere but adds latency and a third-party dependency.

The README does not specify which tunnel service is bundled or recommended. The architecture suggests the backend is tunnel-agnostic: you configure a public URL, and the mobile app uses it.

Connection Stability: WebSocket Reconnect and Command Queuing

The mobile app maintains a WebSocket connection to the backend. If the connection drops mid-task:

  1. The backend continues running the subprocess. It buffers stdout/stderr in memory (up to a configurable limit).
  2. When the mobile app reconnects, it requests the buffered output for the active command.
  3. If the buffer overflows or the backend restarts, the command is marked as failed, and the user must retry.

This is a best-effort design. Long-running agent tasks (e.g., a 10-minute refactor) are vulnerable to network interruptions. The backend does not persist command state to disk, so a restart loses all in-flight work.

Protocol Boundary: Stdio Streams vs. Local APIs

Each provider’s CLI tool exposes a different interface. T3 Code wraps them all with a unified abstraction layer. The backend code likely includes per-provider adapters that translate between the unified API and each CLI’s quirks.

Stdio Wrapping

Most CLI tools (Codex, Claude Code, Cursor) stream output over stdout. The backend:

  1. Spawns the subprocess with stdio: ['pipe', 'pipe', 'pipe'].
  2. Writes the user’s command to stdin.
  3. Reads the agent’s response from stdout line by line.
  4. Parses structured output (JSON, JSONL, or plain text) and forwards it to the client.

This works for request-response commands. It breaks down for interactive agents that expect multi-turn conversations. The backend would need to maintain a persistent subprocess and multiplex stdin/stdout across multiple client connections. The README does not indicate whether T3 Code supports multi-turn interactions.

Local API Calls

Some providers (Grok Build, OpenCode) may expose a local HTTP API instead of stdio. The backend would:

  1. Detect the provider’s API endpoint (e.g., http://localhost:8080).
  2. Send HTTP POST requests with the user’s command.
  3. Stream the response back to the client.

This is cleaner than stdio wrapping but requires the provider to run a persistent daemon. The backend must handle daemon startup, health checks, and shutdown.

Deployment Shape: Electron, Web, and Native Mobile

T3 Code ships as three clients:

  • Electron desktop app: Bundles the backend and web UI into a single binary. The backend runs as a child process of the Electron main process. This is the simplest deployment: download, install, run.
  • Web app: Connects to a backend running separately (via npx t3@latest or a system service). The web app is a static site hosted at app.t3.codes. It uses WebSocket to communicate with the backend.
  • Native mobile apps: iOS and Android apps that connect to a backend running on your laptop. The apps are not Electron wrappers. They are native Swift and Kotlin codebases that share the same WebSocket protocol.

Why Native Mobile Instead of Web Views?

The README does not explain this choice, but the architecture suggests performance and offline capability. Native apps can:

  • Cache command history and UI state locally.
  • Use platform-specific APIs for notifications, background tasks, and biometric authentication.
  • Avoid the latency and battery drain of running a full web view.

The downside: maintaining three codebases (Electron, iOS, Android) instead of one web app. The team chose this trade-off because they wanted “the best possible development experience with agents.”

Observability and Failure Modes

The backend logs to stdout. The Electron app captures these logs and writes them to a file (location varies by OS). The web and mobile apps do not have direct access to backend logs. They rely on error messages returned over the WebSocket connection.

Likely Failure Modes

Failure ModeSymptomRecovery
Provider CLI not installedBackend fails to spawn subprocessInstall CLI and authenticate
Session expiredCLI returns 401 or “not authenticated”Re-run <provider> login
Subprocess crashBackend returns partial output, then silenceRetry command; check backend logs
WebSocket disconnect mid-taskMobile app shows “reconnecting…”Backend buffers output; app fetches on reconnect
Backend restart during commandCommand marked as failedUser retries; no state recovery
Tunnel service down (remote mode)Mobile app cannot connectSwitch to local network or restart tunnel

The backend does not implement automatic retries or circuit breakers. If a provider’s CLI is flaky, the user sees raw error messages. This is acceptable for a developer tool but would not scale to production use cases.

Code Snippet: Subprocess Wrapper Pattern

This is a plausible implementation of the subprocess wrapper. The actual code is not public, but the architecture suggests this pattern:

import { spawn } from 'child_process';
import { EventEmitter } from 'events';

class AgentRunner extends EventEmitter {
  private process: ReturnType<typeof spawn> | null = null;

  async run(provider: string, command: string): Promise<void> {
    const cliPath = this.resolveCLI(provider);
    this.process = spawn(cliPath, ['execute'], {
      stdio: ['pipe', 'pipe', 'pipe'],
    });

    this.process.stdin?.write(command + '\n');
    this.process.stdin?.end();

    this.process.stdout?.on('data', (chunk) => {
      this.emit('output', chunk.toString());
    });

    this.process.stderr?.on('data', (chunk) => {
      this.emit('error', chunk.toString());
    });

    this.process.on('close', (code) => {
      this.emit('done', code);
      this.process = null;
    });
  }

  cancel(): void {
    if (this.process) {
      this.process.kill('SIGTERM');
    }
  }

  private resolveCLI(provider: string): string {
    const paths: Record<string, string> = {
      codex: '/usr/local/bin/codex',
      claude: '/usr/local/bin/claude',
      cursor: '/usr/local/bin/cursor',
      grok: '/usr/local/bin/grok',
      opencode: '/usr/local/bin/opencode',
    };
    return paths[provider] || '';
  }
}

The real implementation likely includes:

  • Path resolution that checks $PATH instead of hardcoding.
  • Timeout handling to kill runaway subprocesses.
  • Output buffering with size limits.
  • Structured logging for each subprocess lifecycle event.

Security Boundaries

T3 Code inherits the security posture of each provider’s CLI. The backend runs with the same permissions as the user who launched it. This means:

  • The backend can read and write any file the user can access.
  • The backend can execute arbitrary code via the CLI tools.
  • The backend does not sandbox or isolate provider CLIs.

The mobile apps connect to the backend over HTTP or WebSocket. If you use the tunnel mode, your commands and agent responses transit the public internet. The README does not mention TLS or end-to-end encryption. This is acceptable for local network use but risky for remote tunneling.

Credential Exposure

The backend does not store credentials, but it reads them from each provider’s credential store. If an attacker gains access to the backend process, they can:

  • Read the credential files.
  • Spawn subprocesses that use those credentials.
  • Exfiltrate agent responses.

The mobile apps do not have direct access to credentials. They send commands to the backend, which handles authentication. This is a reasonable boundary, but it assumes the backend is trusted.

Technical Verdict

Use T3 Code when:

  • You want to control multiple coding agents (Codex, Claude, Cursor, Grok, OpenCode) from a single UI.
  • You prefer a mobile-first experience and are willing to run a local backend.
  • You trust the providers’ CLI tools and are comfortable with subprocess orchestration.
  • You are on a local network or can set up a secure tunnel.

Avoid T3 Code when:

  • You need multi-turn agent conversations with persistent state.
  • You require production-grade observability, retries, or circuit breakers.
  • You cannot run a local backend (e.g., you are on a locked-down corporate machine).
  • You need end-to-end encryption for remote connections.
  • You want a single-vendor solution with tighter integration (e.g., Cursor’s native UI).

T3 Code is a control surface, not a runtime. It does not replace the providers’ CLI tools. It wraps them. This is a strength (you get the latest features from each provider) and a weakness (you inherit their bugs and breaking changes). The project is early (the README warns “expect bugs”), but the architecture is sound for a developer tool that prioritizes flexibility over stability.