I met an Anthropic PM at re:Invent, built a Claude Code relay, and sent him a message asking him to kill it. Two months later /remote and /teleport shipped. Here's the architecture, what I learned, and why getting obsoleted was the plan.
I got a few minutes with a Claude Code PM at a conference, connected on LinkedIn afterward, and a few weeks later sent him a DM that read, more or less: "I built the thing I wish Claude Code did natively. Please kill my product."
Two months later, Anthropic shipped /remote and /teleport. The product is dead. That was the plan.
This is the postmortem for ShellBridge - a privacy-first relay that let you drive Claude Code sessions on any of your dev machines from your phone, your tablet, or somebody else's laptop. I built it because I wanted to answer Claude's prompts from wherever I was, not only from the machine running the agent. Now you can do that in the native app. So ShellBridge is a static landing page pointing at this post, the worker is returning 410s on every API route, and the GitHub repo is archived at tag v1.0.0-final in case anyone ever wants to read how it worked.
Here's what I built in between.
Claude Code, at the time I started, was a terminal app. If you kicked off a long-running agent task on your desktop at home and then walked away, that was it - you were tethered to the machine the agent was running on. You could SSH in from your phone, sure, but:
y/n at 11pm; you find out at 8am.The agentic loop needs a thin, responsive, notification-aware interface that outlives any one terminal session. A mobile web app that can attend to a running agent from anywhere. I wanted that experience in the native Claude Code app. Since it didn't exist yet, I built the workaround - partly because I wanted to use it, partly because showing is easier than telling, partly as a conversation-starter with the people who could ship the real thing.
Three surfaces, privacy-first by design:
Source code never touched the Worker. Terminal I/O bytes flowed through the relay; the actual files and context stayed on the dev machines. The Worker was a dumb-pipe plus an auth/routing layer.
The interesting half of the project. If you've ever thought about building a persistent WebSocket fabric on Cloudflare, this is the shape I'd recommend.
Every session was a node-pty child process on the dev machine, spawned with a controlled environment and cwd. The daemon kept a Map<sessionId, PtyProcess> and fanned bytes from stdout/stderr through the WebSocket as typed SessionUpdate messages.
The non-obvious bit: never strip or re-encode the bytes. Claude Code's UI is a full ANSI terminal with colors, cursor movement, alternate-screen mode, and occasional OSC sequences. The xterm.js client on the other end is already an ANSI interpreter. Any "helpful" processing in the middle breaks the rendering. The daemon's job is to be a hose, not a filter.
Resize is the other surprise. When the PWA resizes its xterm instance (keyboard pops up on iOS, user rotates device), it sends a resize message with the new cols/rows. The daemon calls pty.resize() on the matching process. If you skip that, Claude Code renders at the original terminal dimensions forever and wraps weirdly on mobile.
This is the part I'd happily build again. Durable Objects are a perfect fit for this pattern: stateful, single-instance-per-key, capable of holding WebSockets open on both sides.
┌──────────┐ WS out ┌──────────────────┐ WS in ┌──────────┐
│ daemon │────────────►│ MachineConnection│◄──────────│ PWA #1 │
│ (mac-1) │ │ (DO) │ └──────────┘
└──────────┘ │ │ ┌──────────┐
│ │◄──────────│ PWA #2 │
└──────────────────┘ └──────────┘One DO per machine, keyed by machineId. The daemon's outbound WebSocket is the authoritative feed. Clients connect inbound, authenticate via session cookie, and subscribe to specific sessionIds they're actually viewing. The DO fans messages from the daemon only to clients subscribed to that session, which matters because a single machine might have eight concurrent agent sessions and you're looking at one of them.
A sibling RateLimiter DO held a token bucket per user so a runaway client couldn't spam the daemon. Durable Objects' single-threaded execution model makes token-bucket state trivial - no locking, no races, just a class instance with a storage.get/put and a clock.
The one thing to know: DO hibernation is great for persistent WebSockets, but you have to actually opt in to it (state.acceptWebSocket) and handle the serialized-then-resumed lifecycle. Otherwise you pay wall-clock for every idle connection.
Four message directions, each a discriminated union, all defined in a shared @gateway/shared package with Zod schemas used at every boundary:
DaemonMessage - machine → worker (session output, PTY exit, heartbeats)WorkerToDaemonMessage - worker → machine (input bytes, resize, kill)ClientMessage - PWA → worker (subscribe, input, resize)WorkerToClientMessage - worker → PWA (session list, session update, needs-input signal)Zod at both ends is worth the typing overhead. It caught a dozen subtle protocol drifts during development (e.g. a message I "changed" in one direction but not the other, a field I renamed in the type union but forgot to migrate on the daemon side). The runtime validation failures were clearer than any TypeScript error I would have gotten.
Two auth surfaces, two token types:
cgw_* token. The user creates a token in the PWA, copies it once, and pastes it into the daemon's setup step. The worker stores sha256(token), so even a D1 dump doesn't leak credentials.Splitting these mattered because the daemon runs on the dev machine, possibly in a user-scoped launchd/systemd service, and the human is on a different device. Same user, different trust model, different rotation story.
Probably my favorite piece. The Worker served /install.sh and /install.ps1 - scripts generated at request time from templates. Paste the curl command, and the script would:
~/.local/bin/ (or Windows PATH-equivalent)launchd plist on macOS, a systemd --user service on Linux, a Windows Task Scheduler XML job on Windows~/.bashrc / ~/.zshrc / the PowerShell profile, wrapped in idempotent # >>> ShellBridge >>> marker comments so a re-run wouldn't double-installThe shell hooks shadowed the claude, aider, and codex commands - the shell found the ShellBridge wrapper first, which routed the invocation through the daemon so the session was observable. Bash and zsh used type -P and whence -p respectively to locate the real binary. PowerShell used Get-Command -CommandType Application to skip its own function table and find the actual exe.
It was more engineering than the relay itself. The lesson: cross-platform install is the real product. If it's not one command to install and one command to uninstall, nobody will try it.
VAPID-signed Web Push from the Worker to the PWA when the daemon detected a needs_input pattern in the stream (Claude asking y/n, a permission prompt, an interactive selection). The detection was a regex pass over the PTY output on the daemon; the worker held the browser push subscription in D1 and dispatched the notification.
iOS Safari PWA push reliability was the single worst part of the project. It works, then it doesn't, then it works again. There is no debugging surface. You get a delivery report that says "accepted" and the phone is silent. I eventually added a fallback Web Audio chime that plays when the PWA is foregrounded and detects needs_input over the live socket, which was a better UX on iOS than relying on Apple's push delivery anyway.
Durable Objects as a WebSocket hub is genuinely the right shape. One DO per resource you're fanning out from, hibernation on, Zod schemas on the protocol. I'd reach for this pattern again tomorrow.
D1 is plenty for low-volume SaaS metadata. Users, sessions, machines, daemon tokens, push subscriptions, audit log, usage stats - all of it fit comfortably in D1 for approximately zero dollars. The whole production backend cost under the Workers Paid plan's included quota.
Zod at every boundary paid for itself. Four message directions, one shared package, type safety on both ends plus runtime validation. I never once shipped a protocol mismatch to production.
The monorepo plus pnpm workspaces kept the four surfaces honest. Shared types live in one place. Breaking the protocol breaks all three consumers' builds simultaneously, which is exactly what you want.
iOS PWA WebSocket reconnection during backgrounding. Safari is aggressive about pausing PWAs, and reconnect logic that works great on desktop Chrome feels janky on iOS. I wrote a lot of exponential-backoff-with-jitter code and it was never quite right.
Apple's Web Push delivery on iOS is unreliable enough that I don't trust it as a primary notification channel. The fallback audio chime ended up being the more honest UX.
The billing code was larger than the relay. Stripe webhooks, subscription lifecycle, usage tiers, metered events, customer portal, dunning emails - none of it is hard individually, all of it adds up. For a side project with ambiguous commercial potential, I should have stubbed billing to "everyone's on the free tier" and revisited it when usage warranted.
No real users to confirm the core thesis. I had a small number of free signups and no paying customers when the product got obsoleted. I can tell you the architecture works because I used it daily for three months on my own agents. I can't tell you whether the broader market wanted this, because the market never got to weigh in.
In early 2026, Claude Code shipped /remote - drive a session from another device over an Anthropic-owned backchannel - and /teleport - hand a session between machines. That's the job ShellBridge was doing. No daemon, no relay, no OAuth app, no VAPID keys, no install scripts that generate launchd plists. It's just native.
This is good. I wanted this outcome. Anthropic owns the agent; Anthropic should own the remote-attend surface. A third-party relay in the middle is strictly worse than first-party support: worse latency, more auth surface, more trust you have to ask users to extend, more pieces that can go wrong.
I sent the "please kill my product" message knowing full well that if I was right about the need, a first-party version was inevitable and better. Being right and being obsoleted were the same outcome.
In case you're reading this because you actually had ShellBridge installed and your daemon just started failing:
shellbridge uninstall --forceRemoves the config in ~/.config/shellbridge/, strips the shell hooks from your rc files, unloads the launchd/systemd service, and deletes the CLI wrapper. On Windows, the equivalent wipes the .bat/.ps1 wrappers and unregisters the scheduled task.
On the infrastructure side, shellbridge.io is now a static landing page that serves the marketing copy and links to this post. The Worker returns HTTP 410 Gone on every /api/* route. The MachineConnection and RateLimiter Durable Objects are gone. The D1 database has been exported and deleted. The GitHub OAuth app is revoked. The Stripe webhook is deleted. VAPID keys are rotated.
The GitHub repo is archived at tag v1.0.0-final - the last fully-working build. If you want to resurrect it, clone the tag, restore the secrets listed in .dev.vars.example, and just deploy. It'll work. It's just that you shouldn't.
The usual advice is: don't build on top of a fast-moving platform, because they'll eat your feature. That's good advice when your goal is a durable business.
It's the wrong advice when your goal is to make the platform better, use the thing you built in the meantime, and have an interesting conversation with the people who can ship the native version.
Three things I'd say if you're considering building a thin wrapper around an AI tool in 2026:
If you want a business, don't build a feature the platform owner will obviously ship in 12 months. Pick a problem they won't solve - specific verticals, workflow glue between tools they don't control, data they don't have access to - or make peace with being absorbed.
If you want a conversation-starter and a working tool for yourself in the meantime, build the thin wrapper, ship it polished, send it to the team that should absorb it, and plan the sunset from day one. That's what this was.
ShellBridge wasn't a prototype. It had GitHub OAuth, billing, cross-platform installers, a legal policy stack, push notifications, admin tooling, audit logs. The difference between a polished product and a demo is whether people can actually adopt it. A demo gets ignored. A polished product, even a small one, gives the platform team something real to look at.
If the goal is influence - "here's the UX I think you should ship" - build it to the quality bar that makes ignoring it expensive.
Archive tag, archive branch, exportable data, a reversible deploy strategy, a clean uninstall command for users, a blog post that explains what happened. I wrote the uninstall command before I had any installers to uninstall. I'm glad I did, because the shutdown took an afternoon instead of a week.
The half-life of a side project in a fast-moving space is short. Design the teardown as a first-class feature of the product. Future-you will be grateful.
The code is archived at v1.0.0-final if you want to read it. The Durable Object WebSocket hub pattern is genuinely reusable for any fan-out-from-one-source-to-many-clients problem, not just terminal relays. The install-script generator is worth studying if you've ever had to ship a cross-platform daemon. The auth-split between human sessions and daemon tokens is the right pattern for any tool where the control plane lives on a different device than the thing being controlled.
The product is gone. Go use /remote and /teleport. They're better.