# Fork notes **sz-chrome-remote-extension**: driving a signed-in browser through its companion extension, from other machines. A fork of [tianrendong/pi-chrome](https://github.com/tianrendong/pi-chrome) that lets pi on **other machines** drive one signed-in browser. It adds a remote mode to the pi extension, a standalone broker for the browser's host, and `mcp/`, an MCP server for the same tools. Without `PI_CHROME_REMOTE` set, the pi extension behaves exactly like upstream. Until 2026-10 this repository was named pi-chrome. Not published to npm and not proposed upstream. The package keeps upstream's name, `pi-chrome`, in `package.json`: renaming it would conflict on every rebase. ``` origin https://git.soarez.net/sz/sz-chrome-remote-extension.git upstream https://github.com/tianrendong/pi-chrome.git ``` ## Why the patch exists Upstream only works on the machine running the browser: - the companion extension long-polls a bridge at `127.0.0.1:17318`, hardcoded in `browser-extension/service_worker.js`; - that bridge lives inside whichever pi session started first on the machine, and goes away when that session exits; - a pi session on another host cannot join it. `PI_CHROME_BRIDGE_HOST` set to a remote address makes `listen()` fail with `EADDRNOTAVAIL`, and only `EADDRINUSE` switches a session into client mode, so the session fails at startup. Upstream tried a standalone daemon in 0.16–0.17 (`docs/DESIGN-0.17-daemon.md` in the v0.17.3 tag) and then dropped it; main is back on the 0.15 line. This fork is based on main. ## What the fork changes **`broker/broker.mjs`** runs on the browser's host in place of upstream's in-session bridge. It speaks upstream's HTTP contract to the extension, so the extension is loaded exactly as shipped. Node builtins only, no install step. It has two listeners: | listener | address | routes | |---|---|---| | extension | `127.0.0.1:17318` | `/next`, `/result`, `/status`, `/command` | | peer | `PI_CHROME_BROKER_PEER_HOST`, same port | `/command`, `/status` | Keeping `/next` and `/result` off the peer listener means nothing on the network can drain the extension's queue or forge results. `/command` needs a bearer token on both listeners, because the browser is signed into real accounts. Bind the peer listener to an address on a network you trust (a VPN, say), never to `0.0.0.0`. | Broker setting | Effect | |---|---| | `PI_CHROME_BROKER_PORT` | both listeners' port; default `17318`, which the extension expects | | `PI_CHROME_BROKER_PEER_HOST` | the peer listener's address; unset, there is no peer listener | | `PI_CHROME_TOKEN` | the bearer token | | `PI_CHROME_TOKEN_COMMAND` | a command whose output is the token, for a secret store | **`extensions/chrome-profile-bridge/index.ts`**: every change is marked `FORK:`. | Setting | Effect | |---|---| | `PI_CHROME_REMOTE` unset, or `local` (also `off`, `none`, `0`, `no`, `localhost`) | upstream behaviour | | `PI_CHROME_REMOTE=host:port` | the broker there | | `PI_CHROME_TOKEN`, `PI_CHROME_TOKEN_COMMAND` | the bearer token, as for the broker; read on first use | | `PI_CHROME_AUTHORIZE=indefinite` (or `30m`, `45`…) | skip `/chrome authorize`: every session starts authorized | | `PI_CHROME_REMOTE_LABEL` | how the prompt names the browser; default "the user's own browser on *host*" | | `PI_CHROME_REMOTE_SSH` | the ssh target the prompt gives for file transfer; default the broker's host | | `PI_CHROME_OTHER_BROWSER_TOOLS` | another browser's tools (e.g. `browser_*`), for the prompt to route between | In remote mode the session never binds a port and never promotes itself to bridge. If the broker is unreachable the tools fail. They do not silently drive some other browser on this machine. The system prompt gains a `pi-chrome-remote` section beside upstream's `chrome-profile-bridge` primer, present while `chrome_*` is authorized. It is added by a `before_agent_start` handler of its own, so upstream's handler stays as shipped. It says `chrome_*` drives a signed-in browser on another host, and, when another browser's tools are named, to keep this one for tasks that need the user's sessions. It also covers moving files: uploads are staged on the browser's host with scp, downloads land in its `~/Downloads`, and screenshots are already local. `chrome_upload_file` refuses relative paths in remote mode, because the browser opens them on its own host. Pi sessions on different hosts each get their own automation window. The extension keys windows by pi session id, which is globally unique. Background mode, which is upstream's default, keeps them from focusing the browser or switching its tabs. ## MCP clients `mcp/` serves these tools to Claude Code, Claude Desktop and other MCP clients; its README has the setup. It loads the pi extension from this repository, `../extensions/chrome-profile-bridge/index.ts`, so the server, the extension and the broker always come from one commit. It was its own repository, pi-chrome-mcp, from 2026-09-25 (when it moved out of this fork to stand alone, loading pi-chrome from npm) until 2026-10, when it came back to keep everything that drives the browser through its extension in one place. Its commits were replayed here with `git am --directory=mcp`, linear, so they rebase like any fork commit. It touches no upstream file, so it never conflicts. It keeps its own `package.json` and lockfile; `mcp/run.sh` installs them into `mcp/node_modules`. Run it from a checkout of its own, not from pi's package clone (`~/.pi/agent/git/...`): pi runs `git clean -fdx` there whenever it updates the package, which deletes `mcp/node_modules`. ## Setup On the browser's host: 1. Clone this repository somewhere it will stay. The browser disables an unpacked extension whose folder goes missing, so not a package manager's cache that it may replace. 2. Load the extension once: `chrome://extensions` (or `brave://extensions`) -> Developer mode -> Load unpacked -> `extensions/chrome-profile-bridge/browser-extension`. 3. Run `node broker/broker.mjs` under a supervisor (launchd, systemd), with `PI_CHROME_BROKER_PEER_HOST` and a token set. No pi session on that machine may be running stock pi-chrome, which would take the port first. On every other machine, install the fork as a pi package and set `PI_CHROME_REMOTE=:17318` and the token. ## Taking upstream releases ``` git fetch upstream git rebase upstream/main git push --force-with-lease origin master ``` Then update the broker's checkout, restart it, and reload the extension in the browser. `/chrome doctor` reports a version mismatch until you do. Commits under `broker/` and `mcp/` never conflict. Expect conflicts in the places marked `FORK:` in `index.ts`: the bridge class's bind, forward and status code (including `connectionStatus`, whose `/status` fetch carries the token), `session_start`, the remote prompt-section handler just ahead of upstream's primer, doctor, and `chrome_upload_file`. If upstream changes the extension↔bridge HTTP contract (`/next`, `/result`, the `x-pi-chrome-version` header), port the change to `broker/broker.mjs`. Its request handling is a line-by-line port of the server half of `ChromeProfileBridge`. ## Verifying `/chrome doctor` in a pi session should report "using the pi-chrome broker at http://*host*:17318" and a connected extension. From outside pi: ``` curl -s -H "Authorization: Bearer $PI_CHROME_TOKEN" http://:17318/status ``` `connected: true` with a recent `lastSeenAt` means the extension is polling.