The short version
- Luminair's Chrome extension lets an agent read pages, click, type and take screenshots in your own Chrome, where you are already signed in.
- It clicks through the Chrome DevTools Protocol, so a page sees real input, and it asks for access to your sites only when you first use it.
- Its first link to the app was a WebSocket on a loopback port. Loopback keeps other machines out, not other programs. The same night it moved to Chrome native messaging, and the port was retired for good on 31 August 2026.
- Today a signed helper, a private socket, kernel identity checks and a per-connection secret stand between Chrome and the app, and the agent only gets its Chrome tools after you turn them on for this launch.
Tested on a live page.
The extension was built in one day, 5 August 2026, in Luminair sessions, and every commit that day carries a Claude co-author line. The first version was not checked by reading code. The commit records a run in a real Chrome, 10 of 10 operations against a live page: read it, typed into it, clicked a button and confirmed the page changed, captured a PNG.
That run caught a bug worth knowing about. An error thrown inside a function injected into a page never came back as a failure, so clicking a selector that matched nothing reported success. Injected code now returns its error explicitly and the bridge raises it. An agent that is told “done” when nothing happened is worse than one that fails.
The browser became the agent.
On 21 October 2025 OpenAI launched ChatGPT Atlas, a browser with ChatGPT built in. As TechCrunch reported, “By using ‘agent mode,’ users can ask ChatGPT to complete small tasks in the browser on their behalf.” Perplexity's Comet was already out, and Anthropic's Claude in Chrome extension was in preview.
The same day, Brave's security team published more prompt injection findings in Comet and other AI browsers. In one, instructions were hidden in an image as “faint light blue text on a yellow background”, invisible to the user but read by the assistant. Their summary of the whole category is the sentence to remember: agentic browser assistants can be prompt-injected by untrusted webpage content, “rendering protections such as the same-origin policy irrelevant because the assistant executes with the user's authenticated privileges.”
In November, Anthropic described its own defenses in Mitigating the risk of prompt injections in browser use: training, classifiers that scan untrusted content, and red teaming. It was frank about the limit: “No browser agent is immune to prompt injection”.
Those are problems of what a page can say to a model. There is a second, plainer question that gets less attention: who else can talk to the thing that drives your browser? That part is on the app, and it is the subject of this post.
Input a page cannot tell apart.
The first extension clicked by running el.click() inside the page, which most sites accept and some guarded login forms and rich editors ignore. The last build of the evening switched to the Chrome DevTools Protocol, the interface DevTools itself uses. The extension attaches Chrome's debugger to the tab and sends browser-level input:
// click: move, press, release at the element's centre await dbg('Input.dispatchMouseEvent', tabId, { type: 'mouseMoved', x, y }); await dbg('Input.dispatchMouseEvent', tabId, { type: 'mousePressed', x, y, button: 'left' }); await dbg('Input.dispatchMouseEvent', tabId, { type: 'mouseReleased', x, y, button: 'left' }); // typing: Input.insertText · Enter, Tab, Escape: Input.dispatchKeyEvent
There is a visible cost, and it is a good one. While the debugger is attached, Chrome shows a bar saying Luminair started debugging this browser. If you press Cancel on that bar, Chrome detaches, and the extension falls back to the in-page path. Pages Chrome will not let a debugger touch, such as chrome:// pages and the Web Store, use the fallback too.
An hour earlier the same evening, a change about install-time trust had landed. Access to all sites moved from a permission granted at install to an optional one, requested from the extension's popup the first time you use it, so installing shows no “read and change all your data” warning. The popup offers one button, Give Luminair access to your tabs. Until you press it, reading a page, its HTML or a screenshot fails with an error that tells you where that button is.
To be precise about the limits: that grant covers page reading. Clicks and keystrokes go through the debugger permission, which the extension declares at install, and which Chrome makes visible with its debugging bar whenever the extension is attached.
Loopback keeps out machines, not programs.
The first transport between the extension and the app was a WebSocket server bound to 127.0.0.1, on port 8788 with four spares. The reasoning in the commit is careful. HTTP polling was out, because Chrome stops an idle extension service worker after about 30 seconds and a long poll would kill the bridge. WebSocket traffic resets that timer, so a ping every 20 seconds keeps the link alive.
The same commit also names the weakness, in its own words: “Loopback alone authorizes nothing”, because “any local process can open a socket.” Nothing off your Mac could reach the port. But any program on it, from a stray npm postinstall script to a tool an agent had just downloaded, could connect and say it was the extension. The defence was an approval step: the app refused every operation until you approved the extension's id once.
That is a thin wall for something that clicks in your signed-in browser. An id is a string. A process that can open the port can also send the right string.
No port to scan.
Chrome has a built-in answer: native messaging. The extension calls chrome.runtime.connectNative with a host name. Chrome looks up a small manifest file for that name, checks that the calling extension is listed in its allowed_origins, and only then launches the program the manifest names, talking to it over standard input and output. Nothing listens on the network at all.
This is also how Anthropic wires its own extension to Claude Code. The Claude Code Chrome docs say that the first time you enable the integration, “Claude Code installs a native messaging host configuration file.”
The first commit had ruled native messaging out, because the manifest must name an exact extension id and ids change with how an extension is loaded. The fix was to pin a public key in the extension's manifest, which fixes the id wherever it is loaded from. That made batch B possible, at 22:18 the same night: Chrome launches a tiny relay, the relay pumps bytes to the app over a unix socket, and the socket is guarded by file permissions rather than by a port number.
For a few weeks the WebSocket stayed up as a fallback so an upgrade could never break a working bridge. On 31 August the fallback was retired. The bridge module still exports its old list of ports, now empty, with a comment saying why: so the retired transport “can never accidentally reopen a browser-reachable loopback listener.”
A pipe with several locks.
Batch B still had soft spots, and the file header of today's version lists them by what it no longer has: “no user-writable JavaScript relay or deterministic /tmp socket.” The first relay was a script that ran through the app's own binary, and the socket lived at a path anyone could predict. On 1 September both went. The relay became a compiled, code-signed program, and the connection now has to pass a chain of checks before the app accepts a single message from the browser.
The unusual part is the peer check. A unix socket can tell the listening side the user id and process id of whoever connected, straight from the kernel. The app asks the signed host to read those for the accepted socket, then looks up that process's executable, hashes it, checks its signature, walks to its parent and checks that it is a signed browser. A script that connects to the socket directly fails at the executable check, and there is a test that does exactly that.
The secret is the last lock. The app writes a fresh capability and generation to a private file when it starts listening and again each time a connection ends. The host must prove it knows the capability with an HMAC over a nonce it picked and a challenge the app picked, within three seconds, before any browser bytes are relayed.
Connected is not allowed.
Even a perfect link should not mean the agent can use it. A comment in the app says it plainly: “A connected extension is not authority.” Chrome control has to be armed from the Chrome row of the app's menu-bar panel. The grant lives in memory only. It is never saved, and a disconnect or a relaunch sets it back to off.
Arming is also recorded. Each arm and disarm writes a security event, and if the event cannot be written the switch does not turn on. The Chrome tools are handed to a query only while the extension is connected, its pinned id is verified and the switch is on, so a model never sees tools it could only fail with, or use without you.
Checked, and not claimed.
What this post does not claim
- That Luminair stops prompt injection. This work controls who can drive Chrome. It does not filter what a page says to the model, and we have not measured injection resistance.
- That the bridge works on Windows. The authenticated host is macOS-only today, and the code keeps Chrome control off there until an equivalent host exists.
- That a process running as you, with enough control of your account, could never interfere. The checks raise the bar for impostors; they do not replace keeping your machine clean.
Sources
- TechCrunch · 21 October 2025OpenAI launches an AI-powered browser: ChatGPT Atlas
- Brave · 21 October 2025Unseeable prompt injections in screenshots: more vulnerabilities in Comet and other AI browsers
- Anthropic · 24 November 2025Mitigating the risk of prompt injections in browser use
- Claude Code DocsUse Claude Code with Chrome
Let an agent use your browser, on your terms
Connect the extension, then switch Chrome on for this launch only.