For most of this year I ran OpenCode on my MacBook, usually three or four sessions at once. That works until you close the lid. Every session stops mid-task, and when you open it again the agent has lost the thread.
I wanted agents that keep running while my laptop sleeps, and a way to check on them from my phone. Most of the time an agent only needs a quick "yes, go ahead", and I'm rarely at my desk when it asks.
So I built Otto.
What it does
Paste an API key from Boat, boxd, E2B or Daytona. Otto starts a Linux machine, installs OpenCode, runs it as a service with a random password, and connects to it over HTTPS. The app runs on macOS, Windows, Linux, Android and iOS.
laptop or phone ──HTTPS──▶ cloud machine running OpenCode ──▶ your repo
The app talks to that machine directly. There's no Otto server, no account and nothing to pay me. If you already run OpenCode somewhere, you can connect to it instead.
- Approve from your phone. When an agent wants to run
git push, you get a notification and tap Allow or Deny. - Schedules run overnight. They're cron jobs on the machine, so they run even when all your devices are off.
- Run agents in parallel. Each session shows every step live, and you can stop or redirect any of them.
- Work with the machine. Browse files, open a terminal, and preview dev servers running on it.
Three decisions
I didn't build an agent. OpenCode v2 already has a solid server, with sessions, tools, permissions, MCP and an event stream. I spent my time on the app and the machine.
Tauri, not Electron. One React codebase covers desktop and mobile. The Rust side handles what a webview does badly: streaming HTTP without CORS problems, the OS keychain, native notifications. The Mac app is much smaller than an Electron build would be.
Plain infrastructure. The remote side is a shell script, a systemd unit and a password. When something breaks, I can SSH in and read the logs.
Three bugs
The OOM killer. An agent installed Chromium for Playwright, the machine ran out of memory, and Linux killed the OpenCode server instead of the install. systemd restarted it, so it looked like a random outage. The fix was one line in the service file, OOMScoreAdjust=-900. Now the kernel kills almost anything else first.
Phone keyboards. When the keyboard opens, most mobile browsers shrink the visible area and scroll the whole page up, so the header disappears and the layout jumps. Otto sizes itself to visualViewport.height, undoes that scroll, and drops the bottom safe area while the keyboard is open. The message box now sits on top of the keyboard, like in a native app.
Auto-scroll. "Follow new output unless the user scrolls up" sounds simple. But streaming text, loading images and layout shifts all fire scroll events too. What worked: only real input (wheel, touch, keys) stops the follow. Everything else keeps it, and sending a message always takes you back to the bottom.
What's not done yet
- The Mac build isn't notarized. The install command below skips the warning. If you use the .dmg, open it once, then click Open Anyway in System Settings → Privacy & Security.
- On iOS you have to build it yourself with Xcode.
- There are four providers. Each one is a single file, and the contributing guide explains how to add one.
How it was built
Agents wrote most of the code. I made the decisions: Tauri, no backend, how providers work, how the phone layout should feel, and noticing that deleting a session had no confirmation. Otto is built around the same split. I decide what happens, from wherever I am, and the machine does the typing.
Otto is free and MIT licensed. On a Mac:
curl -fsSL https://jaygupta.dev/otto/install.sh | sh
Windows, Linux and Android builds are at jaygupta.dev/otto. If you find it useful, a star on GitHub helps a lot.