Back to list

Blog

From Prototype to Product: Tactic Remote's Development Journey

13 milestones in 8 weeks took Tactic Remote from a proof-of-concept tmux wrapper to a production-ready remote development tool — here's the full story of that journey.

Published Tags: engineering / milestone / development
Blog

Follow product and engineering updates from this channel.

Browse category

Tactic Remote started as a question in early January 2026: what if you could check on a running Claude Code session from your phone? That question turned into a prototype in a weekend, a usable tool within two weeks, and a product with 13 milestones completed before we shipped our first versioned release. This is the story of that development arc — what we built, what we threw away, and what we learned from shipping fast without shipping broken.

The Weekend Prototype (Early January 2026)

The first version of Tactic Remote was roughly 200 lines of code. A shell script launched Claude Code inside a tmux session, a Python Flask server exposed an endpoint that ran tmux capture-pane and returned the output as plain text, and a basic iOS app polled that endpoint every 2 seconds and displayed the result in a monospaced text view.

It was ugly and slow. The polling model meant 2-second latency on every update. The plain text capture lost all formatting. There was no authentication — anyone on the same Wi-Fi network could read your terminal. But it proved the concept: you could meaningfully follow an AI coding session from a phone screen.

That prototype survived about 4 days before we started rebuilding.

Milestone Architecture

We adopted a milestone-based development model rather than calendar sprints. Each milestone had a specific technical objective, a definition of done, and no fixed deadline. This let us ship when something was ready rather than when a sprint ended. In practice, milestones averaged 4-5 days each, though some took as little as 2 days and one took nearly two weeks.

Here's the condensed progression:

Milestones 1-3: Foundation. Replace Flask with a Swift-native embedded server. Establish WebSocket for real-time streaming instead of HTTP polling. Implement token-based authentication. After Milestone 3, we had a working system where the iPhone received terminal updates within 200ms and unauthorized access was blocked.

Milestones 4-5: Session management. Support multiple concurrent tmux sessions. Build the session lifecycle (create, monitor, idle, complete, cleanup). Add session persistence across companion app restarts by re-attaching to existing tmux sessions on launch. This was the point where Tactic Remote stopped being a demo and became usable for real work.

Milestones 6-7: Human-in-the-loop. Integrate with Claude Code's hook system to intercept approval requests. Build the approval UI on iOS — display what the AI wants to do, show the relevant context, and provide approve/reject actions. Add push notifications so approvals don't require the app to be open. These two milestones were the turning point. Before them, Tactic Remote was a terminal viewer. After them, it was a control surface.

Milestones 8-9: Connection robustness. Handle Wi-Fi to cellular handoffs. Implement automatic reconnection with exponential backoff. Add connection quality indicators. Build the QR code quick-connect flow to eliminate manual IP and token entry. Milestone 9 was the hardest single milestone — networking edge cases on iOS are numerous and poorly documented.

Milestones 10-11: Cloudflare Tunnel. Integrate cloudflared as a managed child process. Build the dual-mode connection architecture (local and tunnel). Handle tunnel health monitoring, automatic restart, and graceful fallback. Add the tunnel URL to QR code generation.

Milestones 12-13: Production readiness. Localization (7 languages). Mac sleep prevention. Menu bar UI polish. Crash reporting and diagnostics. Performance optimization to reduce memory footprint from approximately 60 MB to under 25 MB at idle. Final security audit of the authentication and transport layers.

Key Technical Decisions

Several decisions made during development defined the product's character:

tmux over custom PTY management

We debated this extensively during Milestone 1. Custom PTY management would give us finer control over terminal capture and eliminate the tmux dependency. But it would also mean reimplementing session persistence, terminal emulation, and process lifecycle management — problems tmux has solved over 15 years. We chose to stand on that foundation rather than rebuild it.

WebSocket over Server-Sent Events

For real-time streaming, both protocols work. We chose WebSocket because we needed bidirectional communication — the iPhone sends commands and approvals back to the Mac, not just receives terminal output. SSE would have required a separate channel for upstream communication, adding complexity.

Embedded server over standalone daemon

A standalone daemon (managed by launchd) would have been more "correct" from a macOS architecture perspective. But it would have doubled the installation complexity and created a class of "the server is running but the app isn't" bugs. Embedding the server in the menu bar app means one thing to install, one thing to run, one thing to quit.

Static token over OAuth/session-based auth

Tactic Remote's threat model is straightforward: prevent unauthorized access from the network. A cryptographically random 256-bit token stored in Keychain achieves this without the complexity of token refresh, session expiration, or login flows. For enterprise use cases requiring per-user auth, we recommend Cloudflare Access as an external layer.

Polling tmux over filesystem watches

We poll tmux capture-pane at 200ms intervals rather than using filesystem notification APIs (kqueue/FSEvents) on a PTY. This sounds inelegant, but tmux's capture command is extremely fast (sub-millisecond), and filesystem watches on PTY devices proved unreliable across macOS versions in our testing. The polling approach has been rock-solid across macOS 13, 14, and 15.

What We Threw Away

Not everything survived. Some notable cuts:

A web-based client. We built a responsive web interface during Milestone 6 that could run in any browser. It worked, but the experience was meaningfully worse than a native app — no push notifications, no background reconnection, no haptic feedback on approvals. We shelved it to focus on the native iOS experience.

Terminal color and formatting. Early versions attempted to preserve ANSI color codes and render them on the iPhone. The rendering was inconsistent and added significant complexity to the streaming protocol. We switched to a simplified rendering model that preserves structure without attempting pixel-perfect terminal emulation. Full ANSI support remains on the roadmap.

Automatic task suggestions. We prototyped a feature where Tactic Remote would suggest Claude Code tasks based on your recent git activity. It was technically interesting but felt presumptuous — developers know what they want to work on. We removed it before any beta tester saw it.

The Versioning Transition

After Milestone 13, we transitioned from milestone-based tracking to semantic versioning. The first versioned release was 1.2.0 (not 1.0.0, because we had internal builds numbered 1.0 and 1.1 during closed testing that we didn't want to create confusion around).

The progression since: 1.2.0, 1.3.0, 1.4.0, 1.4.2, and 1.5.0-beta.1. Each version maps to a clear set of user-facing changes documented in our changelog. The milestone system served us well during rapid prototyping, but semantic versioning communicates more clearly to users who need to evaluate upgrade risk.

What We Learned

Ship the smallest useful thing. The weekend prototype was embarrassing by any engineering standard, but it answered the only question that mattered: does monitoring Claude Code from a phone actually help? Every milestone after that was an incremental answer to "what's the next most useful improvement?"

Networking is the hard part. We spent more time on connection reliability than on any other aspect of the product. Wi-Fi to cellular handoffs, WebSocket reconnection after sleep, Cloudflare Tunnel health monitoring — each of these consumed a full milestone. The actual terminal capture and session management was comparatively straightforward.

Dependencies should be boring. tmux, cloudflared, Keychain, IOKit — the technologies Tactic Remote depends on are stable and well-documented. Every time we considered adopting something newer or more novel, we asked whether the existing boring option was good enough. It usually was.

Milestone-based development works for small teams. Without the overhead of sprint planning, standups, and velocity tracking, we moved faster. Each milestone had a clear finish line, and we could adjust scope in real-time based on what we learned during implementation. This model breaks down at larger team sizes, but for our early development it was right.

Timeline Summary

PeriodWhat Happened
Early Jan 2026Weekend prototype: shell script + Flask + basic iOS app
Jan 6-20Milestones 1-5: Foundation, WebSocket, sessions
Jan 20-29Milestones 6-9: Approvals, push notifications, QR connect
Jan 29 - Feb 10Milestones 10-11: Cloudflare Tunnel integration
Feb 10-18Milestones 12-13: Localization, polish, security audit
Feb 18+Semantic versioning: 1.2.0 through 1.5.0-beta.1

Eight weeks from an idea to an open beta. Not because we cut corners, but because we cut scope relentlessly and built on proven foundations.

For a detailed look at each versioned release, see our Changelog.

Try Tactic Remote

Control your coding Agents from your phone

Connect to Claude Code, Codex, and other Agents on your Mac, Windows, or Linux computer. Check progress and send the next instruction from iPhone or iPad.