Jay Gupta
← Writing

Building Otto: my agents live on a cloud box now

11 Oct 2026 · 3 min read

My setup for most of this year: OpenCode running on my MacBook, three or four sessions going at once, me reviewing and steering. It works great until you close the lid. Then everything dies mid-task, and you come back to a half-written migration and an agent that has no idea what happened.

I wanted two things. Agents that keep running when my laptop sleeps. And a way to check on them from my phone, because half the time the agent just needs one "yes, go ahead" and I'm not at my desk.

So I built Otto.

What it actually is

You paste an API key from a sandbox provider (Boat, boxd, E2B or Daytona). Otto spins up a Linux machine, installs OpenCode, runs it as a systemd service with a random password, and exposes it over the provider's HTTPS proxy. Then the app talks to it. Desktop, iOS, Android, same app.

That's it. No backend of mine in the middle, no account, nothing to pay me for.

Decisions I'm happy with

Don't build the agent. OpenCode v2 already has a solid server: sessions, tools, permissions, MCP, an event stream. My job was the client and the machine, not another agent loop. Every week I don't spend on agent internals is a week on the part people actually touch.

Tauri, not Electron. One React codebase for desktop and mobile, a Rust side for the stuff browsers are bad at. The Rust HTTP client means no CORS headaches and real streaming, secrets go into the OS keychain, notifications are native. The macOS build is a fraction of what Electron would be.

Boring infrastructure. The whole remote side is a shell script, a systemd unit and a password. No Kubernetes, no custom daemon. When something breaks on a sandbox, I can SSH in and read it. Boring is a feature when the thing has to survive your laptop being closed for 8 hours.

The bugs that taught me something

The OOM killer. Someone asked an agent to use Playwright, the agent ran npx to install Chromium, the sandbox ran out of memory, and Linux killed... the OpenCode server. Not the install. The thing everything depends on. The sandbox showed "running, but no open port" and then fixed itself when systemd restarted it.

Fix was one line: OOMScoreAdjust=-900 on the service, so the kernel picks literally anything else first. Took an hour to find, two seconds to fix. Classic.

Mobile keyboards are cursed. When the keyboard opens on a phone, most browsers and webviews shrink the visual viewport and then scroll the whole page up to show the input. Your header vanishes, the layout jumps. I ended up sizing the app to visualViewport.height myself, undoing the browser's scroll, and collapsing the bottom safe area while the keyboard is up. Now the composer just rides on top of the keyboard like in a native app.

Auto-scroll is about intent. "Stick to the bottom unless the user scrolled up" sounds simple. It isn't, because streaming content, images loading and layout shifts all fire scroll events too. What finally worked: only user input (wheel, touch, keys) can unpin the view. Everything else re-pins. And sending a message always brings you back down.

How it got built

Agents wrote most of the code. I'm not shy about that. What they didn't do: pick Tauri, decide there'd be no backend, design the provider abstraction, decide what the phone layout should feel like, or notice that "confirm before delete" was missing.

That's the split I like. Agents are fast hands. I'm the one deciding where the hands go, and reviewing everything they touch. It's the same reason Otto exists: I want to make the calls from wherever I am, and let the machine do the typing.


Otto is MIT licensed and runs on macOS, Windows, Linux, iOS and Android. If it's useful to you, a star helps a lot. Issues and ideas are welcome.

Read next
Stop grading your agents yes/no