Skip to content

PRD: OpenCode Connect menu-bar app #1

Description

@avoidthekitchen

Problem Statement

Using OpenCode Web from an iPhone currently requires manually starting a server, exposing it safely through Tailscale, finding the correct URL, and repeating or troubleshooting those steps whenever a process exits, the Mac sleeps, or network state changes. The commands are individually manageable, but their lifecycle and security boundaries are easy to get wrong. A user can accidentally expose OpenCode to the LAN, overwrite unrelated Tailscale Serve configuration, leave stale access behind after quitting, or start duplicate processes after a crash.

The target user wants a lightweight personal macOS utility that makes private mobile access routine: start it from the menu bar, scan a QR code once, bookmark a stable Tailscale URL, and return across sessions. It must remain low-configuration while using strong defaults appropriate for an agent that can access local files and execute shell commands.

Solution

Build OpenCode Connect, a native menu-bar-only macOS application that manages a loopback-only OpenCode server and an app-owned private Tailscale Serve route. The app exposes one context-sensitive control surface for setup, Start, Stop, retry, conflict resolution, QR enrollment, credentials, settings, and diagnostics.

OpenCode Connect will separate persisted Desired State (Enabled or Disabled) from Observed State (Stopped, Needs Setup, Starting, Available, Degraded, Stopping, Conflict, or Error). It will reconcile the machine toward the user's intent without adopting, terminating, or overwriting resources it cannot safely recognize. Available means the complete authenticated path through the Tailscale Endpoint has been verified, not merely that an OpenCode process exists.

Protected Access will be the default: tailnet authorization plus a persistent OpenCode Basic Auth Access Credential stored in the Mac Keychain. Tailnet-Only Access will remain an explicit advanced alternative. OpenCode will always bind to 127.0.0.1; Tailscale Funnel and direct LAN exposure will not be supported.

User Stories

  1. As a Mac user, I want to start private OpenCode access from the menu bar, so that I do not need to remember terminal commands.
  2. As a Mac user, I want to stop access from the menu bar, so that I can intentionally remove remote access when I am finished.
  3. As an iPhone user, I want to scan a QR code containing the verified Endpoint, so that I can open OpenCode without typing a long tailnet URL.
  4. As an iPhone user, I want to bookmark a stable node-based Endpoint, so that I usually need to scan the QR code only once.
  5. As an iPhone user, I want the app to warn me if the Endpoint changes, so that I know an existing bookmark may no longer work.
  6. As an iPhone user, I want to use OpenCode's existing project picker, so that I can select arbitrary local projects without a second project browser in OpenCode Connect.
  7. As a security-conscious user, I want OpenCode bound only to loopback, so that devices on my LAN cannot bypass Tailscale.
  8. As a security-conscious user, I want Tailscale Funnel unsupported, so that the app cannot accidentally publish OpenCode to the public internet.
  9. As a new user, I want Protected Access enabled by default, so that an unexpectedly broad tailnet policy does not immediately expose my coding agent.
  10. As a new user, I want a strong generated Access Credential, so that I do not need to invent a secure password.
  11. As a returning user, I want the Access Credential stored in the Mac Keychain, so that OpenCode can restart securely without prompting me on the Mac.
  12. As an iPhone user, I want a readable six-word passphrase with Copy and Reveal actions, so that first-time Basic Auth enrollment is manageable.
  13. As an iPhone user, I want the URL QR code kept separate from the Access Credential, so that scanning or sharing the URL does not disclose the password.
  14. As an iPhone user, I want Safari to be able to save the Basic Auth login, so that subsequent visits have little authentication friction.
  15. As an advanced user, I want to choose Tailnet-Only Access with an explicit warning, so that I can avoid redundant Basic Auth when I accept the reduced defense in depth.
  16. As a Protected Access user, I want to rotate the Access Credential, so that I can recover from suspected disclosure.
  17. As a user rotating credentials, I want a warning that my iPhone's saved login must be updated, so that the resulting authentication failure is not surprising.
  18. As a user switching access modes, I want the stored Access Credential preserved, so that returning to Protected Access does not require unnecessary re-enrollment.
  19. As a user, I want credential deletion to be separate from Reset to Defaults, so that routine settings recovery does not unexpectedly break my saved mobile login.
  20. As a new user, I want the app to locate separately installed OpenCode and Tailscale executables, so that setup is usually automatic.
  21. As a user with a nonstandard installation, I want to select custom executable paths, so that the app can work without owning dependency installation.
  22. As a user with a missing dependency, I want a specific diagnostic and installation guidance, so that I know what to fix.
  23. As a signed-out Tailscale user, I want a specific remediation message, so that authentication failure is not confused with an OpenCode failure.
  24. As a first-time Tailscale Serve user, I want a user-initiated Complete Tailscale Setup action, so that I can grant one-time HTTPS approval through the official flow.
  25. As an iPhone user, I want setup guidance to state that Tailscale must also be connected on the phone, so that I understand the complete prerequisite.
  26. As a user, I want the app to discover the Endpoint from Tailscale, so that it does not guess my machine or tailnet DNS name.
  27. As a user, I want to copy the Endpoint, so that I can send it to my own device through another trusted channel.
  28. As a user, I want to open the Endpoint from the menu, so that I can verify the same HTTPS route locally.
  29. As a user, I want a concise Observed State in the menu bar, so that I can understand availability at a glance.
  30. As a user, I want Available to require authenticated end-to-end verification, so that a green status does not merely mean a process exists.
  31. As a user, I want Degraded to distinguish temporary reachability problems from a fully failed setup, so that I know recovery may occur automatically.
  32. As a user, I want Needs Setup to distinguish missing prerequisites from runtime errors, so that remediation is clear.
  33. As a user, I want transitional Starting and Stopping states, so that I know a command is still progressing.
  34. As a user, I want errors to identify the failed stage and show a useful remediation, so that I do not need to interpret raw CLI output.
  35. As a user, I want bounded automatic retries for transient failures, so that temporary problems recover without an infinite retry loop.
  36. As a user, I want Retry and Stop actions after automatic retries are exhausted, so that I remain in control.
  37. As a user, I want failed startup to preserve Desired State as Enabled, so that the app does not silently discard my intent.
  38. As a user, I want partial startup rolled back after failure, so that neither a newly created route nor server is left behind accidentally.
  39. As a user, I want Stop to remove the Managed Route before terminating the Managed Server, so that remote access closes promptly.
  40. As a user, I want normal Quit to set Desired State to Disabled and clean up, so that quitting does not leave intended access running.
  41. As a user restarting after an app crash, I want the app to reconcile surviving resources, so that it does not create duplicate OpenCode servers or routes.
  42. As a user with another process on the configured port, I want a Conflict instead of automatic termination, so that OpenCode Connect does not kill unrelated work.
  43. As a user with existing Tailscale Serve configuration, I want the app to refuse to overwrite an occupied HTTPS port or root route, so that unrelated services remain intact.
  44. As a user, I want cleanup to remove a Managed Route only while its target still matches, so that external route changes are not destroyed.
  45. As an advanced user, I want to choose another HTTPS Serve port while access is Disabled, so that I can resolve a legitimate port conflict.
  46. As an advanced user, I want to choose another OpenCode backend port while access is Disabled, so that I can resolve a local conflict.
  47. As a security-conscious user, I do not want a configurable bind address, so that settings cannot bypass the loopback-only invariant.
  48. As a user, I want operational settings locked while access is Enabled, so that configuration changes cannot create partial live states.
  49. As a user, I want Reset to Defaults while Disabled, so that I can recover standard ports, executable discovery, and access mode safely.
  50. As a user, I want Launch at Login to default Off, so that installation does not silently add startup behavior.
  51. As a user enabling Launch at Login, I want Enabled intent restored after login, so that remote access returns after reboot without manual intervention.
  52. As a user leaving Launch at Login off, I want logout or shutdown to disable intent and attempt cleanup, so that a later manual launch does not unexpectedly restore old access.
  53. As a user, I want sleep to preserve Desired State, so that waking the Mac can restore availability automatically.
  54. As a mobile user, I want a clear statement that a sleeping or closed-lid Mac may be unreachable, so that I understand the availability limit.
  55. As a user, I want an Availability Policy of On External Power, Always, or Never, so that I can balance remote availability and battery usage.
  56. As a user, I want On External Power to be the default Availability Policy, so that plugged-in mobile access remains available without routinely draining the battery.
  57. As a user, I want the app to use a native power assertion, so that keeping the Mac awake does not require an extra caffeinate process.
  58. As a user, I want a warning when Enabled on battery while sleep remains possible, so that unexpected mobile disconnection is less likely.
  59. As a user, I want no Dock icon or permanent main window, so that the utility remains lightweight.
  60. As a user, I want one context-sensitive primary action, so that setup, startup, retry, conflict, and stop workflows remain understandable.
  61. As a user, I want component status for OpenCode, Tailscale, and keep-awake behavior, so that I can diagnose availability without reading implementation details.
  62. As a user, I want notifications only for user-relevant Error or Conflict outcomes, so that the utility does not create notification noise.
  63. As a privacy-conscious user, I want no analytics, crash uploads, or network telemetry, so that local development activity remains private.
  64. As a user seeking help, I want Copy Diagnostics to produce sanitized state and version information, so that I can share useful evidence deliberately.
  65. As a security-conscious user, I want credentials, authorization headers, QR contents, and complete environments excluded from logs, so that diagnostics do not disclose secrets.
  66. As a user, I want to review diagnostics before sharing them, so that potentially sensitive local paths are not sent unintentionally.
  67. As a maintainer, I want enhanced logging deferred behind a future opt-in flag, so that the MVP defaults remain private and bounded.
  68. As a source-build user, I want clear README build instructions, so that I can compile the app without relying on a binary release.
  69. As a binary-download user, I want clear README installation and Gatekeeper instructions, so that unsigned or ad-hoc-signed releases are usable.
  70. As a user, I want supported macOS versions and architectures documented explicitly, so that compatibility expectations are clear.
  71. As a release consumer, I want published checksums, so that I can verify downloaded artifacts.
  72. As a user, I want manual GitHub release updates for the MVP, so that the app does not need a privileged automatic updater.
  73. As an open-source contributor, I want dependency and lifecycle boundaries documented, so that future changes preserve the security model.

Implementation Decisions

  • Build a native SwiftUI macOS menu-bar application with no Dock icon and no permanent main window. Use a conventional Settings window for configuration.
  • Introduce one application-level Access Coordinator as the primary orchestration and testing seam. It accepts user and system events and publishes a complete view model containing Desired State, Observed State, Endpoint, component health, and remediation.
  • Keep UI code thin. It sends intentions to the Access Coordinator and renders coordinator output; it does not directly launch processes, mutate Tailscale configuration, access Keychain secrets, or manage power assertions.
  • Define dependency adapters for OpenCode process control, Tailscale CLI interaction, Keychain access, persisted settings/runtime records, power source and sleep assertions, system lifecycle events, clock/retry scheduling, notifications, and local diagnostics.
  • Persist Desired State separately from Observed State. Desired State values are Enabled and Disabled. Observed State values are Stopped, Needs Setup, Starting, Available, Degraded, Stopping, Conflict, and Error.
  • Start sets Desired State to Enabled. Stop and explicit Quit set it to Disabled. Failure does not silently change Enabled intent.
  • Startup order is: validate settings and dependencies, reconcile existing resources, launch or adopt the Managed Server, verify authenticated local health, create or adopt the Managed Route, discover the Endpoint, verify authenticated HTTPS access through it, then publish Available.
  • Stop and rollback order is: remove the matching Managed Route first, terminate the Managed Server gracefully, force termination only after a bounded timeout and only when ownership is still verified, release any power assertion, then publish Stopped.
  • Automatic retries are bounded and limited to transient failures. When exhausted, publish Error, retain Enabled intent, and wait for explicit Retry or Stop during that app session. A later manual launch performs a fresh bounded reconciliation attempt.
  • Launch OpenCode with opencode serve, hostname 127.0.0.1, and backend port 4096 by default. Do not use mDNS or opencode web.
  • Set OpenCode Basic Auth environment variables only in Protected Access. The default username is opencode; custom usernames must be non-empty.
  • Generate a cryptographically secure, readable six-word Access Credential on first Protected Access setup. Store it as a generic password in the Mac Keychain and never persist it in ordinary preferences or runtime records.
  • The URL QR code contains only the verified Endpoint. Credentials are shown separately through explicit Reveal and Copy actions. Do not embed URL user-info or credentials in QR payloads.
  • Tailnet-Only Access is an advanced setting with an explicit reduced-defense warning. Switching away from Protected Access retains the Access Credential unless the user separately deletes it.
  • Rotate Credential requires Desired State Disabled, replaces the Keychain secret, and warns that saved browser credentials must be updated.
  • Bind address is a non-configurable 127.0.0.1 invariant. Tailscale Funnel, direct LAN exposure, and public endpoints are unsupported.
  • Configure Tailscale Serve as a private background HTTPS reverse proxy from port 443 by default to the loopback OpenCode backend. The public HTTPS port and backend port may be changed only while Disabled.
  • Discover the Endpoint from Tailscale's reported Serve state rather than constructing it from guessed machine or tailnet names. Persist the last verified Endpoint only to detect and explain changes.
  • Use the Mac's normal Tailscale node identity. Tailscale Services, custom service identities, multi-Mac failover, and movable endpoints are deferred.
  • Before writing Serve configuration, inspect the current configuration. Refuse to overwrite an occupied port or root route. Never use a global Serve reset.
  • Record the precise Managed Route target. Adopt or remove it only when the live mapping still matches the expected loopback target and port.
  • Persist a bounded runtime record for the Managed Server, including enough identity to verify PID, canonical executable path, active port, and authenticated health without persisting the Access Credential.
  • On launch after a crash, adopt a surviving Managed Server only when the persisted identity, executable, and authenticated endpoint agree. Never adopt or terminate a process based only on its name or occupied port.
  • When evidence is ambiguous, publish Conflict and block automatic startup and cleanup. Provide evidence and safe remediation; any destructive cleanup requires explicit user authorization.
  • OpenCode owns Project Selection. Launch from the user's home directory as a predictable process working directory, but do not build a project picker, default-project setting, or filesystem-confinement promise.
  • Require OpenCode and Tailscale to be installed separately. Do not bundle, install, update, sign in, or broadly reconfigure either dependency.
  • Search known Homebrew, standalone Tailscale, and App Store Tailscale CLI locations and allow custom executable selection. Force the macOS Tailscale application binary into CLI mode when required by its supported environment contract.
  • Validate dependency identity and versions before use. Present specific Needs Setup remediation for missing executables, invalid custom paths, Tailscale sign-out/disconnection, and one-time Serve HTTPS authorization.
  • Launch the official Tailscale approval URL only from an explicit Complete Tailscale Setup action, then retry validation.
  • Launch at Login defaults Off. If enabled, login starts the menu app and reconciles toward persisted Desired State. If disabled, logout or shutdown sets Desired State to Disabled and attempts cleanup. Explicit Quit always disables and cleans up.
  • Sleep preserves Desired State. Wake triggers reconciliation while the app is running.
  • Implement Availability Policy values On External Power, Always, and Never. Default to On External Power. Use native macOS activity/power assertion APIs and release assertions when policy, power source, Desired State, or process lifecycle no longer requires them.
  • Do not claim that an assertion keeps a closed-lid MacBook available. Surface this limitation in onboarding and settings.
  • Use a template menu-bar icon with distinguishable normal, transitional, and attention-required presentations. Do not depend on color alone to communicate status.
  • The popover contains status, explanation, one primary action, Endpoint actions, QR enrollment, compact component health, and secondary Settings, Diagnostics, Reset, and Quit actions.
  • Show notifications only when a user-initiated Start or recovery ends in Error or Conflict. Do not notify for routine transitions.
  • Operational settings—including executable paths, ports, access mode, username, credential rotation, and reset—are editable only while Desired State is Disabled. Launch at Login and non-operational presentation preferences may change at any time.
  • Reset to Defaults restores executable auto-detection, backend port 4096, HTTPS port 443, Protected Access, default username, and On External Power availability. It does not delete the Access Credential or mutate active resources.
  • Use local macOS unified logging plus bounded captured command output. Redact secrets before persistence or display. Do not enable verbose OpenCode logging by default.
  • Copy Diagnostics includes app, OS, OpenCode, and Tailscale versions; detected executable paths; sanitized Desired and Observed State; sanitized component health and route target; failure stage; and recent lifecycle events. Warn that local paths may be sensitive and provide review before copying.
  • Do not add analytics, remote telemetry, automatic crash uploads, or an automatic updater in the MVP. A future opt-in enhanced logging flag may be added when real debugging needs justify it.
  • Distribute outside the Mac App Store without App Sandbox. Enable Hardened Runtime where compatible with unsigned/ad-hoc distribution. Do not bundle OpenCode or Tailscale.
  • The initially required and tested platform is macOS 26 on Apple Silicon. Keep implementation compatible with macOS 14 and Intel where straightforward, but do not delay the MVP or claim support until those combinations are verified.
  • Provide an open-source Xcode project and README documentation covering supported systems, prerequisites, source builds, configuration, Protected Access enrollment, Tailscale setup, normal use, power limitations, troubleshooting, binary installation, Gatekeeper's Open Anyway flow, and security boundaries.
  • Initial releases may be source-only followed by an ad-hoc-signed ZIP. Add an unsigned DMG after lifecycle behavior stabilizes. Publish checksums. Developer ID signing, notarization, and paid Apple Developer membership are deferred until adoption justifies them.
  • Do not add an always-running supervisor/helper in the MVP. A launchd-supervised lifecycle agent is a v2 option if field experience shows that manual-restart reconciliation is insufficient.

Testing Decisions

  • Use the Access Coordinator as the single primary behavioral seam. Tests send user/system events and assert published state, remediation, and externally visible side effects rather than internal helper calls or SwiftUI implementation details.
  • Exercise the coordinator with deterministic adapters for OpenCode, Tailscale, Keychain, persistence, power, system lifecycle, clock/retry scheduling, notifications, and diagnostics.
  • Cover the full happy path from Disabled/Stopped through Enabled/Starting to Available, including authenticated local health, route creation, Endpoint discovery, and end-to-end HTTPS verification.
  • Cover Stop and Quit cleanup ordering, graceful and forced process termination, route-first access closure, power assertion release, and final Disabled/Stopped state.
  • Cover startup failures at every stage and assert rollback of only newly created resources, preservation of Enabled intent, bounded retries, and stable Error output.
  • Cover manual launch after a simulated crash with all combinations of surviving/missing Managed Server and Managed Route. Assert safe adoption only when identity and authenticated health agree and assert that duplicates are never launched.
  • Cover PID reuse, changed executable path, occupied backend port, modified Serve target, occupied HTTPS route, and unverifiable process ownership. Assert Conflict and no destructive mutation.
  • Cover Protected Access creation, Keychain reuse, missing credential repair, credential rotation, access-mode changes, and secret deletion. Assert that secrets never enter ordinary persistence, diagnostics, logs, QR payloads, or command arguments.
  • Cover dependency discovery across supported installation variants, custom executable validation, missing dependencies, invalid binaries, Tailscale disconnected/signed-out states, and one-time HTTPS approval remediation.
  • Cover Endpoint changes, DNS/TLS propagation delays, Tailscale offline transitions, sleep/wake transitions, and the distinction between Available, Degraded, Needs Setup, and Error.
  • Cover Launch at Login and logout/reboot policy for both Enabled and Disabled intent, including cleanup attempts when login launch is disabled.
  • Cover Availability Policy transitions across AC and battery power, ensuring assertions are acquired and released correctly and never imply closed-lid support.
  • Cover settings immutability while Enabled, Reset to Defaults semantics, preservation of the Access Credential, and controlled application of port changes after Disabled state.
  • Cover sanitation with hostile command output containing passwords, authorization headers, environment values, usernames, and sensitive paths. Assert diagnostics remain useful without exposing secrets.
  • Add focused contract tests around parsing recorded OpenCode and Tailscale CLI output/configuration fixtures because external formats are outside the app's control. These tests support, rather than replace, coordinator behavior tests.
  • Keep SwiftUI testing limited to smoke coverage of status rendering, context-sensitive primary actions, QR presentation, disabled operational settings, and accessibility labels. Business and lifecycle behavior belongs at the Access Coordinator seam.
  • The repository currently has no application tests or prior art. Establish coordinator scenario tests as the canonical pattern for subsequent lifecycle work.
  • Include a small manual release checklist for real OpenCode/Tailscale interoperability, iPhone QR enrollment, Safari credential saving, sleep/wake, reboot, Gatekeeper installation, and cleanup of the actual Serve configuration. These validate platform integration that deterministic tests cannot fully reproduce.

Out of Scope

  • Tailscale Funnel, public internet exposure, direct LAN exposure, configurable non-loopback binding, or custom TLS termination.
  • Installing, bundling, updating, authenticating, or replacing OpenCode or Tailscale.
  • A custom authentication proxy, Tailscale identity-header authorization, passkeys, OAuth, or an iOS companion application.
  • Embedding credentials in the Endpoint or QR code.
  • A custom project browser, favorite projects, default project selection, or filesystem sandboxing beyond OpenCode's own behavior.
  • Multiple simultaneous OpenCode servers, multiple Endpoints, multiple Macs, movable Tailscale Services, failover, multiple tailnet users, or phone selection.
  • Guaranteed availability while the Mac sleeps or its lid is closed, Wake-on-LAN, or remote power management.
  • An always-running launchd supervisor/helper or guaranteed immediate cleanup after the menu app itself crashes.
  • Automatic destructive resolution of process, port, or Serve conflicts.
  • Live mutation of operational settings while access is Enabled.
  • Remote analytics, telemetry, crash reporting, or default verbose logging.
  • Automatic application updates.
  • Mac App Store distribution, App Sandbox support, bundled dependencies, Developer ID signing, or notarization.
  • Guaranteed Intel or macOS 14 support in the first release; compatibility is best effort until verified and documented.
  • A polished DMG as a blocker for the initial source or ZIP release.

Further Notes

  • The primary deployment is one MacBook, one tailnet user, and one iPhone, while avoiding hard-coded identity or path assumptions so other users can configure their own equivalent setup.
  • Removing and recreating the Managed Route does not normally change the node-based Endpoint. Renaming the Tailscale machine or tailnet can change it and invalidate bookmarks.
  • Normal Stop and Quit cleanup can be made reliable, but a process cannot guarantee cleanup after its own crash. The MVP deliberately provides conservative reconciliation on the next manual launch rather than adding a persistent supervisor.
  • OpenCode currently serves its web UI through opencode serve, which avoids opencode web automatically opening a browser. The dependency version check and real interoperability release checklist should detect upstream behavior changes.
  • The README is part of MVP completion, not optional follow-up documentation. It must state exactly which OS and architectures have been verified rather than implying best-effort combinations are supported.
  • The domain glossary and architectural decisions define canonical terminology and the non-sandboxed, loopback/Tailscale-only boundaries that implementation must preserve.

Metadata

Metadata

Assignees

No one assigned

    Labels

    ready-for-agentFully specified, ready for an AFK agent

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions