Jayesh Suryavanshi.

Field Guide

The browser is a peripheral

Claude Code can drive Chrome. The documentation is good about what it can do and quiet about how. So I read the binary, the extension bundle and the tool server’s own logs, and found five hops, two transports, and one thing about where page text goes that I did not expect.

Abstract

The agent never touches the browser. It emits a tool call that crosses five hops to reach a page, and the page answers as either an accessibility tree or a screenshot. Reading the shipped CLI, the extension bundle and the tool server’s session logs turns up two separate transports rather than one, a screenshot coordinate frame that disagrees with its own declared scale about a third of the time, and the fact that on this path the page content leaves the machine for a hosted relay as clear JSON inside TLS. Roughly half of the boundary cannot be verified on macOS at all. This note says which half, and why that matters more than the architecture diagram.

I use the browser tools in Claude Code most weeks, usually for the boring end of the job: check that a deploy actually rendered, read a console error out of a page that only breaks when logged in, fill a form I do not want to fill again. It works well enough that I had never asked what happens in between. Then somebody asked me and I found I could describe the capability and not the mechanism, which is the tell that I had been reading documentation instead of the thing.

How I checked, and what I could not

Everything below about the command-line side is read out of the shipped CLI, version 2.1.278, plus the tool server’s own session logs, which it writes in the clear under ~/Library/Caches/claude-cli-nodejs/, and the live process and socket table. That part I consider evidenced.

The browser side is not in the same class. macOS file protection blocks every read under the Chrome profile directory, even with the sandbox off: stat works, opening a file does not. So the extension analysis comes from a fresh Web Store download of the same version string as the installed copy, not from the installed bytes. I did not confirm the two are byte-identical, and I never captured a live frame on the wire. Where I am reading code rather than watching behaviour, I say so.

Five hops to a click

The useful reframing is in the title. The model is not in the browser and does not reach into it. It emits a tool call, the same shape as any other tool call, and something downstream turns that into a click. The browser is a device on the end of a wire.

one tool callpath
model
 └─ read_page / computer / navigate ...
     └─ chrome tool server   # in-process, inside the CLI. not a subprocess
         └─ wss://bridge.<vendor-relay>/chrome/<account>   # leaves the machine
             └─ extension service worker   # MV3, host_permissions <all_urls>
                 ├─ content script: accessibility-tree.js
                 └─ chrome.debugger   # clicks, typing, screenshots
                     └─ the page

Two details in there are worth pulling out. The tool server runs inside the CLI process, not as a child of it: the session log says In-process Chrome MCP server started and then reports a stdio transport connecting in four milliseconds, which is an in-memory pair, not a pipe.1 And the content script is injected at document_start, in every frame, on every URL. Nothing about that is unusual for an extension of this kind, but it is the answer to “when does it start looking”: before your page runs.

How a page becomes something a model can read

There are two channels, and they are not interchangeable.

The structured one returns an accessibility tree with opaque reference tokens, ref_0, ref_1, and so on. It is capped: fifteen levels deep, ten thousand elements, fifty thousand characters. Password fields and hidden inputs come back as a redaction marker rather than a value. A second tool skips the tree and walks ten content selectors in priority order, first match wins, falling back to the whole body.

The visual one returns a screenshot as a short text handle plus a separate base64 image block. Across my own transcripts those ran from about three kilobytes to a hundred and sixty-seven, almost always JPEG.

What crossesShapeTargeting
Accessibility treetext, capped and redactedref_N tokens
Extracted page texttext, selector-chosennone
Screenshothandle + base64 imagex, y in the image frame
Console, networktext, filterablenone

The reference system is the interesting design choice, and I think it is the right one, because the coordinate story does not hold up. Going back over eighty-one annotated screenshots in my own sessions, twenty-eight had a delivered width that did not match the scale the annotation itself declared. The same “0.6 scale” label produced ratios of 0.600, 0.667 and 0.800 on different captures.2 The screenshot frame is also not reliably the same as the viewport the structured read reports. If you are wiring these tools into anything you intend to trust, prefer a reference over a coordinate wherever the tool offers both. Pixels are a fallback, not a foundation.

Where the page actually goes

This is the part I did not expect, and the part I got wrong on the first pass before checking it properly.

I assumed the browser was driven locally. There is every reason to think so: the CLI registers a native messaging host with seven different browsers, and a matching helper process really does sit there holding a unix socket with private permissions, one per process id. That machinery exists, it works, and the code that would encrypt payloads end to end lives on exactly that path.

It is not the path the CLI uses. The transport selector checks for a bridge configuration first, and the factory that builds the context sets that configuration unconditionally, so the websocket branch always wins and the local socket pool is unreachable from this client. It also never sets the options that would turn on the sealing, the proactive dialing or the peer-credential check. On my machine the live socket has exactly one holder, the desktop app’s helper, and nothing connected to it. Two transports exist. Which one you are on depends on which client you are running, and the answer for the command line is the remote one.

So the frame goes out over a websocket to a vendor-hosted relay, addressed by account, authenticated by an access token in the opening message. Tool calls and their results are matched by a request id. And the sealing does not apply: every reference to it in that module sits in the local-socket pairing code, none in the websocket client, which builds its frame and serialises the arguments as they are. Page text, accessibility trees and base64 screenshots cross as clear JSON inside TLS to a machine that is not yours.

I read that out of the binary rather than off the wire, so treat it as a strong code-level claim and not a packet capture. It is also not a vulnerability and I am not presenting it as one. TLS is doing its job and the relay is the vendor’s own. But it does change the answer to a practical question, which is which tabs you point this at. “The agent reads the page locally” would have been a reasonable thing to assume and it is not what happens.

What is actually watching

Everything on the return path is written by the page. Not just the obvious text and screenshots, but the console, the network bodies, the tab titles and URLs appended to nearly every result, and, my favourite, the confirmation string after a form fill, which helpfully echoes the field’s previous value. A hostile page controls all of it.

There are gates, and they sit in different places: per-site grants and blocked categories inside the extension, an approval mode with a classifier that inspects each outgoing call, and account identity on the connection itself. Documented protected actions still interrupt even on a site you have blanket-approved, and a documented prohibited list refuses outright, including the nicely circular entry for completing instructions found in web content.

The gap worth naming: nothing in the command-line binary inspects the content coming back. The probes that scan tool results for injections are real and documented, but they are not in this artifact, which carries one generic warning sentence in the system prompt and a classifier pointed at the outgoing call. That layer lives elsewhere, server side or on another surface. If your mental model was “something on my laptop is checking the page before the model reads it”, that is not what I found.

The published robustness numbers deserve the same care. There are at least four separate evaluations across about a year: an early suite of a hundred and twenty-three cases, an adaptive best-of-N attacker, a harness the vendor retired because it had been saturated, and a current red-team suite. The grader changed. The denominator changed to attacks that reached the model. Quoting the early figure and the current one as a single trend line would be wrong, which is the same provenance problem I wrote about in the closed frontier: the number is fine, the comparison is the thing that breaks. The vendor’s own framing is the honest one, that a low single-digit success rate still represents meaningful risk.

If you are about to use it

Prefer references over pixel coordinates. Treat every returned string as untrusted input, including the ones that look like the tool talking rather than the page. Assume the content of any tab you open leaves the machine. And keep a separate browser profile for it, which is the vendor’s own recommendation and the cheapest control available.

The honest close is the limit rather than the finding. The command-line half of this I can stand behind, because I read the running artifact and its own logs. The browser half I read from a store download, because the operating system would not let me open the copy that is actually running. Two halves of one system, described at two very different confidences, and the one users care about is the weaker one. If somebody can read the installed bundle on a machine without that restriction, the in-page mechanism is where I would look first.

Bottom line

  • The model drives the browser as a device, not as a browser. Five hops from tool call to page, with the tool server running inside the CLI process rather than beside it, and a content script already injected before your page runs.
  • Two transports exist and the command line uses the remote one. The local native-messaging path is real, works, and is where the end-to-end encryption lives, but the bridge config is set unconditionally so the local pool is never reached.
  • Page content leaves the machine in clear JSON inside TLS. Read from the binary, not from the wire. Not a vulnerability, but it changes which tabs you should point this at.
  • Nothing local checks what comes back. Result-scanning probes are documented and are not in this artifact. Every returned string, including tool-sounding confirmations, is page-authored.
  • Prefer references to coordinates. A third of the screenshots I checked disagreed with their own declared scale, and the image frame is not reliably the reported viewport.

Sources

  • Primary: the shipped CLI 2.1.278, its mcp-logs-claude-in-chrome session logs, the live process and socket table, and the extension bundle at the version string installed here. Read on macOS, September 20, 2026.
  • Browser tool surface and known failure modes: code.claude.com/docs/en/chrome
  • Permission model, protected and prohibited actions, blocked categories: permissions guide · using it safely
  • Injection evaluations and defence layers, across four different suites: Aug 2025 · Nov 2025 research · GA post
  • Companion piece: The closed frontier, on reading provenance before reading numbers.