All posts

My AI Assistant Got a Browser That Lives at Home

A Raspberry Pi on a home desk running a browser, with a robot assistant reaching down from a cloud

My AI assistant handles a lot of my web errands: checking packages, filing claims, activating credit card offers. Its hosted browser usually works fine. But Amex’s login button would sometimes spin forever, with no error or timeout. USPS would show “Access Denied” on a tracking page. Amazon would ask for a CAPTCHA.

The same accounts and actions worked from my laptop. The hosted browser was coming from a datacenter IP and carried automation fingerprints; my laptop was browsing from home.

Someone on r/MuseAgent had posted a setup that addressed both: a headed browser on a residential IP, with the webdriver flag hidden and trusted-input clicks. I had a Raspberry Pi sitting around unused, so I tried it.

Why I used the Pi

I considered a residential proxy first. It would fix the IP problem without any hardware, but I’d be paying a monthly bill when I could use the Pi I already owned for $0. A proxy also wouldn’t fix how the browser runs, which bot defenses care about too. On the Pi, I could run Chromium through its normal graphical path, on my home connection, with a persistent profile. I liked having the whole setup under my control.

The assistant still runs on its hosted VM. It sends instructions to Chromium on the Pi, which opens pages, runs JavaScript, holds cookies, and makes network requests. The two machines talk over the Chrome DevTools Protocol (CDP). The agent fetches a debugger WebSocket URL from the Pi and uses it to control tabs.

As far as a website is concerned, it’s a headed Chromium browser with its own profile, connecting from my house.

How it works: cloud AI connects securely to the home Pi, which browses sites as a normal home visitor

Setting up Chromium

It’s a Raspberry Pi 5 running Ubuntu, which I manage over SSH without a desktop. Codex helped with this part. Xvfb and the Ubuntu snap version of Chromium were already installed. We added Tailscale and joined the Pi to my tailnet before making the browser controls reachable.

CDP gives extensive control over the browser and has no login. The externally reachable control endpoints listen only on the Tailscale address. There’s no LAN listener, router port forwarding, or public internet exposure.

Xvfb is a virtual X server: it provides a screen in memory. Ours is 1920×1080, and Chromium runs headed on that display, using its full graphical path without a physical monitor. It doesn’t need a desktop environment or window manager. The browser runs unattended, with the agent driving it over CDP.

We verified that launching Chromium with automation-detection features disabled makes navigator.webdriver evaluate to false. That changes one signal. Sites can still inspect other browser properties and behavior, so it doesn’t make the browser undetectable. I’d be skeptical of anyone claiming otherwise.

Chromium also ignored our request to bind the debugger to the Tailscale address and listened on localhost instead. We found that by checking the actual listening sockets. A small TCP relay fixed it: Chromium keeps its localhost listener, and the relay accepts connections on the Tailscale address and debugger port, then forwards them to Chromium. Both the discovery requests and the debugger WebSocket work through it, while external access stays restricted to the tailnet.

The snap package caused another problem. The Chromium snap can’t write to arbitrary hidden directories, including the path we’d chosen for the browser profile. We made that path a symlink to a location the snap can write to. The profile keeps cookies, local storage, and logins across restarts and tasks. It’s separate from the profiles on my laptop and phone; using the same home connection doesn’t bring their identities or logins with it.

The pieces run as systemd user services, enabled at boot with lingering so they don’t need an interactive login. They restart on failure with a backoff. The launcher waits for a valid Tailscale address, prevents duplicate launches with a lock, and checks the debugger endpoint’s health. If the browser becomes unhealthy, the launcher exits so systemd can restart it.

Logging in through the viewer

We added an optional VNC connection bridged to a browser-based viewer, also bound to the Tailscale address. I can open it from my phone or laptop, watch the browser, and use its mouse and keyboard.

The viewer shares the agent’s browser session. When a site needs me to log in, I do it myself through the viewer. My assistant can’t and shouldn’t read my passwords. The profile saves the login, and the agent can continue in the same session. Being able to take over that browser feels much simpler than trying to pass credentials around. It’s how we got the Amex task done.

A person and an AI assistant sharing control of one browser window

Connecting the agent

The assistant’s VM joined the same tailnet, which I approved from my phone. But the VM can’t reach the tailnet directly: its connections have to go through the runtime’s HTTPS proxy.

The CDP driver is a small Node script. It fetches the debugger endpoint, opens a tab, and connects over WebSocket to navigate pages, take screenshots, and send trusted input. Those are browser input events through CDP’s Input domain, rather than clicks synthesized in page JavaScript.

Getting the WebSocket through the proxy took two fixes. Normal forwarding couldn’t carry the WebSocket Upgrade, so the driver opens an explicit HTTP CONNECT tunnel first. Then it has to hand the connected socket to the WebSocket client through a callback after the tunnel is established. Returning the socket synchronously failed with Unexpected server response: 101. The client was treating the success code as an error.

Ping doesn’t help test this route because it only carries TCP. We had to test the actual connection.

We wrote a short operator’s guide with endpoints, troubleshooting, and attach examples. It includes a detail that’s easy to miss: fetch the debugger WebSocket URL from the HTTP endpoint before each connection. The URL contains an ID that changes every time Chromium restarts. This Chromium also requires PUT, rather than GET, to open a new tab through its API. Both details had to be handled for the driver to work.

The guide also spells out which traffic goes through the Pi. Pages opened in Chromium make their requests from home. Other network calls made by agent code on the VM still use the VM’s connection. The Pi hosts a remote-controlled browser, not a VPN or general-purpose exit node.

Trying it on the sites that had blocked us

On October 3, I loaded a simple page and captured a clean screenshot to check the full path: VM, proxy, tailnet, Pi, browser. That worked end to end.

USPS tracking loaded without “Access Denied.” The package I’d been unable to check for days had been delivered to my front door the day before.

Amazon loaded without a CAPTCHA. Search worked with typed input, autocomplete appeared, and the page located the session in my home city.

Amex was the most useful result. Its login would silently spin forever in the managed browser because the bot defenses flagged that environment rather than my account. Through the Pi, I logged in using the viewer, then let the agent activate every available offer on both of my cards. There were hundreds on one card and a handful on the other, with none left unclaimed. Amex logged us out once after five idle minutes. I logged back in through the viewer, and the agent continued. That logout was session cleanup, not a fraud flag.

Using it day to day

The assistant still tries its normal managed browser first to keep routine tasks fast. As soon as it gets blocked by bot defenses, an access-denied page, or a login that keeps spinning, it automatically retries through the Pi without asking me or announcing the switch.

Running headed Chromium instead of headless on my home connection has helped more than hiding the automation flag. It works for the sites I use, but fingerprinting is still an arms race.

I still need to add a Tailscale access rule that restricts the debugger to the agent’s VM node. It’s reachable only over the tailnet today, but it isn’t yet limited to that specific node.

The project took an afternoon and used a Pi I already had. The credit card offers it activated on the first day will cover the electricity for a long time.