Skip to content

Instantly share code, notes, and snippets.

@iurysza
Created March 13, 2026 14:59
Show Gist options
  • Select an option

  • Save iurysza/57d6fc1aca85d500c0eb236842f51c1e to your computer and use it in GitHub Desktop.

Select an option

Save iurysza/57d6fc1aca85d500c0eb236842f51c1e to your computer and use it in GitHub Desktop.
OpenCode always-on web + launchd attach setup

OpenCode always-on web + launchd attach setup

This is the current setup pattern I use on my Mac, with machine-specific values redacted.

What it does

  • Keeps one OpenCode backend running under launchd.
  • Makes bare opencode attach a TUI to that shared backend.
  • Keeps normal subcommands like opencode run, opencode auth, and opencode upgrade unchanged.
  • Exposes the same backend to Android over Tailscale Serve HTTPS.
  • Cleans up an old tmux-based backend only if one is still hanging around.

Files

Scripts

  • ~/scripts/opencode-web/service.sh
  • ~/scripts/opencode-web/shell.sh
  • ~/scripts/opencode-web/launchd-run.sh

Local config

  • ~/.config/opencode-web/env

LaunchAgent

  • ~/Library/LaunchAgents/com.<user>.opencode-web.plist

Local state

  • ~/.local/state/opencode-web/

Shell integration

~/.zshrc contains:

source ~/scripts/opencode-web/shell.sh

Env file

Path: ~/.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-me

Meaning:

  • ENABLED=1 wraps bare opencode.
  • TAILSCALE_SERVE=1 makes backend bind to localhost and lets Tailscale Serve front it.
  • OPENCODE_SERVER_PASSWORD is optional.

Backend runtime

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.sh

That runner:

  • sources ~/.config/zsh/.exports.zsh
  • loads ~/.config/opencode-web/env
  • picks backend host:
    • 127.0.0.1 when TAILSCALE_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

Wrapper behavior

~/scripts/opencode-web/shell.sh does this:

  • resolves the real opencode binary 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:
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.

Tailscale behavior

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

Auth

Basic auth is optional.

  • if OPENCODE_SERVER_PASSWORD is 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.

Commands

~/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 doctor

status 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.

Example healthy state

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: disabled or set, depending on env

Quick checks

~/scripts/opencode-web/service.sh status
~/scripts/opencode-web/service.sh doctor
launchctl print gui/$(id -u)/com.<user>.opencode-web
tailscale serve status
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment