This is the current setup pattern I use on my Mac, with machine-specific values redacted.
- Keeps one OpenCode backend running under
launchd. - Makes bare
opencodeattach a TUI to that shared backend. - Keeps normal subcommands like
opencode run,opencode auth, andopencode upgradeunchanged. - Exposes the same backend to Android over Tailscale Serve HTTPS.
- Cleans up an old tmux-based backend only if one is still hanging around.
~/scripts/opencode-web/service.sh~/scripts/opencode-web/shell.sh~/scripts/opencode-web/launchd-run.sh
~/.config/opencode-web/env
~/Library/LaunchAgents/com.<user>.opencode-web.plist
~/.local/state/opencode-web/
~/.zshrc contains:
source ~/scripts/opencode-web/shell.shPath: ~/.config/opencode-web/env
ENABLED=1
TMUX_SESSION=opencode-web
PORT=4096
TAILSCALE_SERVE=1
# Optional. Uncomment to enable basic auth.
# OPENCODE_SERVER_PASSWORD=replace-meMeaning:
ENABLED=1wraps bareopencode.TAILSCALE_SERVE=1makes backend bind to localhost and lets Tailscale Serve front it.OPENCODE_SERVER_PASSWORDis optional.
The backend is managed by launchd, not tmux.
~/scripts/opencode-web/service.sh start writes or refreshes a LaunchAgent plist, bootstraps it if missing, and kickstarts it if needed.
The LaunchAgent runs:
~/scripts/opencode-web/launchd-run.shThat runner:
- sources
~/.config/zsh/.exports.zsh - loads
~/.config/opencode-web/env - picks backend host:
127.0.0.1whenTAILSCALE_SERVE=1- current Tailscale IPv4 otherwise
- runs:
opencode web --hostname "$backend_host" --port "$PORT"With TAILSCALE_SERVE=1, the backend listens on:
http://127.0.0.1:4096
~/scripts/opencode-web/shell.sh does this:
- resolves the real
opencodebinary once - loads
~/.config/opencode-web/env - if
ENABLED != 1, passes through directly - if args are present, passes through directly
- if no args are present:
- runs
~/scripts/opencode-web/service.sh start - attaches with:
- runs
opencode attach http://127.0.0.1:4096 --dir "$PWD"If OPENCODE_SERVER_PASSWORD is set, the wrapper exports it for the attach call. If it is unset, attach runs without auth.
Direct IP access to the backend is intentionally gone when TAILSCALE_SERVE=1.
This no longer listens:
http://<tailscale-ip>:4096
Tailscale Serve fronts the backend over HTTPS and proxies to http://127.0.0.1:4096.
Example node DNS name:
<device-name>.<tailnet>.ts.net
Example web URL:
https://<device-name>.<tailnet>.ts.net
Basic auth is optional.
- if
OPENCODE_SERVER_PASSWORDis set, OpenCode web prompts for auth - username is
opencode - password comes from
~/.config/opencode-web/env
In my current live setup, OPENCODE_SERVER_PASSWORD is commented out, so auth is disabled.
~/scripts/opencode-web/service.sh start
~/scripts/opencode-web/service.sh stop
~/scripts/opencode-web/service.sh restart
~/scripts/opencode-web/service.sh status
~/scripts/opencode-web/service.sh doctorstatus is the main operator view. It prints:
- env file path and state
- feature toggle state
- Tailscale Serve toggle state
- launchd label, plist, state, and pid
- legacy tmux session state
- Tailscale IPv4
- Tailscale DNS name
- attach URL
- Serve URL
- Serve state
- process state
- port listener state
- next suggested action
doctor runs status plus HTTP probes and extra failure hints.
When everything is working:
- feature enabled:
1 - tailscale serve:
1 - launchd service:
loaded - launchd state:
running - attach url:
http://127.0.0.1:4096 - tailscale serve url:
https://<device-name>.<tailnet>.ts.net - tailscale serve state:
configured - password:
disabledorset, depending on env
~/scripts/opencode-web/service.sh status
~/scripts/opencode-web/service.sh doctor
launchctl print gui/$(id -u)/com.<user>.opencode-web
tailscale serve status