OpenCode plugin that makes any OpenCode server accessible via Tailscale Funnel for mobile sync. Works everywhere OpenCode runs — Windows, macOS, Linux — and with every OpenCode client (Desktop, TUI, web, CLI headless).
v1.1.0 — Native toast + full cross-platform. The plugin now prefers the built-in
client.tui.showToast({ body: { message, variant } })API and only falls back to OS notifications when the TUI is unavailable. OS fallback is itself cross-platform: Windows (WinRT toast → balloon tip), macOS (osascriptNotification Center), Linux (notify-send→kdialog→zenity).
On startup, the plugin automatically:
- Runs first-time setup if needed (password file, desktop patch on Windows, Tailscale Funnel)
- Launches the desktop sidecar if port 4096 is not listening and the desktop is installed
- Falls back to the CLI server (
opencode serve) if the desktop isn't running — keeps mobile access alive from device boot (now via direct spawn on macOS/Linux, PowerShell on Windows) - Starts the auto-repatch watcher (Windows) to re-apply desktop patches after updates
- Injects
OPENCODE_PORT=4096andOPENCODE_SERVER_PASSWORDinto shell commands - Checks for plugin updates hourly from GitHub releases
- Notifies via native in-app toast first, OS notification second (never both)
| Platform | Server mode | Native toast | OS fallback | Funnel |
|---|---|---|---|---|
| Windows 10/11 | Desktop sidecar (patched app.asar) or CLI opencode serve |
client.tui.showToast (any client) |
WinRT toast → NotifyIcon balloon |
tailscale funnel 4096 |
| macOS 13+ | CLI opencode serve (direct spawn) |
client.tui.showToast |
osascript Notification Center |
tailscale funnel 4096 |
| Linux | CLI opencode serve (direct spawn) |
client.tui.showToast |
notify-send → kdialog → zenity |
tailscale funnel 4096 |
All clients share the same session DB path (~/.local/share/opencode/opencode.db on POSIX, %LOCALAPPDATA%-equivalent on Windows via opencode).
The desktop sidecar is the primary server on Windows (serves the proper web UI with full functionality). But the desktop client may be closed, updating, or never launched — and the mobile app should still work from device boot.
The CLI server (opencode serve) serves the same web UI and uses the same database. The plugin checks port 4096 and only starts the CLI server if nothing else is listening. On macOS/Linux it spawns opencode serve directly with OPENCODE_SERVER_PASSWORD and OPENCODE_SERVER_CORS; on Windows it delegates to start-opencode-server.ps1.
Open OpenCode Desktop and paste this prompt:
Install the mobile-sync plugin from https://github.com/neohiro/mobile-sync:
1. Clone: git clone https://github.com/neohiro/mobile-sync.git "$env:TEMP/mobile-sync"
2. Copy plugin: Copy-Item "$env:TEMP/mobile-sync/mobile-sync.js" "$env:USERPROFILE\.config\opencode\plugins\"
3. Copy scripts: New-Item -ItemType Directory -Path "$env:USERPROFILE\.config\opencode\plugins\mobile-sync-scripts" -Force; Copy-Item "$env:TEMP/mobile-sync/scripts/*.ps1" "$env:USERPROFILE\.config\opencode\plugins\mobile-sync-scripts\"
4. Verify: Confirm plugins/mobile-sync.js and plugins/mobile-sync-scripts/*.ps1 both exist
After install, restart OpenCode. The plugin auto-loads like auto-resume.js.
macOS / Linux:
git clone https://github.com/neohiro/mobile-sync.git /tmp/mobile-sync
mkdir -p ~/.config/opencode/plugins
cp /tmp/mobile-sync/mobile-sync.js ~/.config/opencode/plugins/
# scripts are Windows-only helpers; plugin works without them on POSIXAfter restart, verify the plugin loaded by checking for [mobile-sync] in the logs, or run:
# Windows
Get-Content "$env:USERPROFILE\.local\share\opencode\log\opencode.log" -Tail 50 | Select-String "mobile-sync"# macOS / Linux
tail -n 50 ~/.local/share/opencode/log/opencode.log | grep mobile-syncTo get your mobile app connection details (Windows):
& "$env:USERPROFILE\.config\opencode\plugins\mobile-sync-scripts\show-connection.ps1"On macOS/Linux:
cat ~/.opencode-server-password # password
tailscale status --json | jq .Self.DNSName # funnel URLWindows:
git clone https://github.com/neohiro/mobile-sync.git "$env:TEMP\mobile-sync"
Copy-Item "$env:TEMP\mobile-sync\mobile-sync.js" "$env:USERPROFILE\.config\opencode\plugins\"
New-Item -ItemType Directory -Path "$env:USERPROFILE\.config\opencode\plugins\mobile-sync-scripts" -Force
Copy-Item "$env:TEMP\mobile-sync\scripts\*.ps1" "$env:USERPROFILE\.config\opencode\plugins\mobile-sync-scripts\"macOS / Linux:
git clone https://github.com/neohiro/mobile-sync.git /tmp/mobile-sync
cp /tmp/mobile-sync/mobile-sync.js ~/.config/opencode/plugins/# Windows
New-Item -ItemType Directory -Path "$env:USERPROFILE\.config\opencode\plugins\mobile-sync-scripts" -Force
Copy-Item mobile-sync.js "$env:USERPROFILE\.config\opencode\plugins\"
Copy-Item scripts\*.ps1 "$env:USERPROFILE\.config\opencode\plugins\mobile-sync-scripts\"# POSIX
mkdir -p ~/.config/opencode/plugins
cp mobile-sync.js ~/.config/opencode/plugins/Clone this repo and add to your config with the full path:
{
"plugin": ["C:/path/to/mobile-sync/mobile-sync.js"]
}{
"plugin": ["/home/you/mobile-sync/mobile-sync.js"]
}Install structure:
~/.config/opencode/plugins/
auto-resume.js (existing)
mobile-sync.js <-- plugin entry (flat, auto-discovered) — cross-platform
mobile-sync-scripts/ <-- PowerShell helpers (Windows only, optional on POSIX)
setup-opencode-shared.ps1
patch-opencode-desktop.ps1
start-opencode-desktop.ps1
start-opencode-server.ps1
watch-opencode-desktop.ps1
show-connection.ps1
- Any OS: Windows 10/11, macOS 13+, or Linux (x64/arm64)
- OpenCode CLI (
winget install SST.opencode/brew install/npm i -g @opencode-ai/cli) - OpenCode Desktop — optional, Windows only. CLI fallback works everywhere without it.
- Tailscale installed and logged in (same tailnet on phone + computer)
- Python 3.12+ — only for Windows desktop patch (
app.asar); not needed for CLI-only mode on any OS
Mobile App (Android)
|
v
Tailscale Funnel (https://<device>.<tailnet>.ts.net)
|
v
Patched Desktop Sidecar (port 4096) <-- primary on Windows, if desktop running
OR
CLI Server `opencode serve` (port 4096) <-- fallback everywhere (direct spawn on POSIX)
|
v
Local Session DB (~/.local/share/opencode/opencode.db) <-- shared
|
v
Toast: client.tui.showToast() succeeds? -> done (no OS toast)
otherwise -> OS notifier (WinRT / osascript / notify-send)
The plugin patches the desktop client's app.asar with two changes (Windows only, only if desktop is present):
- Password: Accepts
OPENCODE_SERVER_PASSWORDenv var (was always random UUID) - CORS: Allows configured origins for Tailscale Funnel (was only
oc://renderer)
If the desktop is not installed or not running, the plugin starts opencode serve instead — no patching required. On POSIX this is a direct spawn("opencode", ["serve", ...], { env: { OPENCODE_SERVER_PASSWORD, OPENCODE_SERVER_CORS } }) with no PowerShell.
The plugin bundles these PowerShell scripts in mobile-sync-scripts/ (Windows helpers — POSIX uses direct spawn):
| Script | Purpose | Platform |
|---|---|---|
setup-opencode-shared.ps1 |
First-time setup (password, patch, funnel) | Windows |
patch-opencode-desktop.ps1 |
Patch desktop app.asar |
Windows |
start-opencode-desktop.ps1 |
Launch patched desktop | Windows |
start-opencode-server.ps1 |
CLI server fallback | Windows (POSIX spawns directly) |
watch-opencode-desktop.ps1 |
Auto-repatch after updates | Windows |
show-connection.ps1 |
Show connection details for mobile app | Windows (POSIX: cat ~/.opencode-server-password + tailscale status) |
- Preferred:
client.tui.showToast({ body: { title, message, variant: "info" | "success" | "warning" | "error", duration: 5000 } })— renders inside every OpenCode client (desktop TUI, terminal, web). Synchronous: if it returnstrue/ no-throw, the OS toast is cancelled. - Fallback: OS notification — Windows
Windows.UI.Notifications→NotifyIconballoon; macOSosascript display notification; Linuxnotify-send→kdialog→zenity. Only runs when the native toast is unavailable (headlessserve, older server, no TUI attached).
The plugin checks for updates on startup and hourly:
- Fetches latest release from GitHub
- Compares version numbers
- Downloads and replaces files if newer version available
- Logs "Restart OpenCode to load" after update
- Shows a native toast (or OS fallback) on successful update
Each device runs the plugin independently with its own session database. The mobile app connects to one device at a time via that device's Tailscale Funnel URL.
To set up a new device:
- Install OpenCode (desktop or CLI)
- Install Tailscale and log in
- Clone this repo and install the plugin (Options 1-4 above)
- Restart OpenCode — the plugin auto-runs setup on first launch
- Run
show-connection.ps1(Windows) orcat ~/.opencode-server-password(POSIX) to get the device's mobile app connection details - Add the device to your shared registry (optional, Windows):
.\show-connection.ps1 -Register
- On any device, list all registered devices (Windows):
.\show-connection.ps1 -AllDevices
Same password on all devices is recommended for easy switching. The password file is at ~\.opencode-server-password (Windows) / ~/.opencode-server-password (POSIX) and is generated on first run.
URLs are device-specific — the plugin dynamically detects the full Tailscale DNS name (e.g., laptop-xyz.taild879f3.ts.net) from tailscale status --json. No hardcoded hostnames.
| Constant | Value | Description |
|---|---|---|
| Port | 4096 |
Server HTTP port (hardcoded, matches Tailscale Funnel) |
| Password file | ~/.opencode-server-password |
Shared auth password (same on all devices) |
MOBILE_SYNC_ENABLED |
1 (default) |
Set to 0, false, or off to disable the plugin without removing it |
MOBILE_SYNC_DESKTOP_ONLY |
0 (default) |
Set to 1 to never fall back to CLI server (desktop only) |
To disable the plugin, either set MOBILE_SYNC_ENABLED=0 in your environment or rename plugins/mobile-sync.js to plugins/mobile-sync.js.disabled.
- Verify Tailscale Funnel:
tailscale funnel status - Verify server is running:
Test-NetConnection -ComputerName 127.0.0.1 -Port 4096(Windows) /nc -zv 127.0.0.1 4096(POSIX) - Check connection details:
& "$env:USERPROFILE\.config\opencode\plugins\mobile-sync-scripts\show-connection.ps1"(Windows) /tailscale status --json(POSIX) - Check that CORS matches the Funnel URL — the
--corsflag must match your Tailscale DNS name exactly - Check logs:
Get-Content "$env:USERPROFILE\.local\share\opencode\log\opencode.log" -Tail 50 | Select-String "mobile-sync"/tail -n 50 ~/.local/share/opencode/log/opencode.log | grep mobile-sync
The CLI server and desktop sidecar share the same DB, but only when launched from the same home directory. The plugin launches opencode serve from $HOME so all projects are visible. If you started a manual opencode serve from a different directory, its project root may be that directory only. Symptoms:
- Android shows empty → server's
location.directory(viashow-connection.ps1→Directory:field) does not contain your projects. Fix: - Stop the manual server, let the plugin relaunch it (or run
start-opencode-server.ps1/opencode servefrom$HOME). - Ensure the mobile app's Directory field matches
show-connection.ps1's Directory output (e.g.,C:\Users\Wouton Windows).
Check if Python is installed (required for asar patch):
python --versionCheck if the desktop is already running:
Get-Process -Name "OpenCode"# Windows
Get-NetTCPConnection -LocalPort 4096 | Select-Object OwningProcess
Stop-Process -Id <PID># POSIX
lsof -i :4096
kill <PID>Check the log for import errors:
Get-Content "$env:USERPROFILE\.local\share\opencode\log\opencode.log" -Tail 100 | Select-String "mobile-sync|error|ERROR"tail -n 100 ~/.local/share/opencode/log/opencode.log | grep -E "mobile-sync|error"The plugin file must be at the top level of ~/.config/opencode/plugins/ (not in a subdirectory) for auto-discovery to find it. See install structure above.
MIT