A browser extension is split between the page and the browser

How does a browser extension do what a website can't? It is a small website of its own, given permissions, and its code runs in two places: a script inside the page, which can change what you see, and a worker in the browser, which holds the browser's powers. A feature that needs both sends a message across.

  1. 01The question
  2. 02An extension is a small website with a contract
  3. 03Its code runs in two places
  4. 04A feature that needs both sends a message
  5. 05Driving the browser takes DevTools' channel

01The question

A website can only change itself. Its JavaScript can't read another tab, block the ads on a different site or see which address you have open. Browsers keep it that way on purpose: otherwise any page could spy on every other.

Yet an ad blocker strips every site you visit, a password manager fills forms on all of them, and an AI agent can click through a page you're building. These are extensions, and they are the one sanctioned way past that wall. So what is an extension, and how does it reach a page that isn't its own?

02An extension is a small website with a contract

An extension is HTML, CSS and JavaScript in a folder, the same material as any site. Chrome gives it an address of its own, chrome-extension:// followed by a long id, so its files load like a tiny private website.

What makes it more than a website is one required file, its : manifest.json. It names the extension, lists the permissions it wants (tabs, storage) and the sites it may reach (https://*.github.com/*), and says which of its files run where. The warning you see at install time, "Read and change your data on github.com", is that list read back to you. Chrome lets the code do nothing the manifest didn't ask for.

The format in use is Manifest V3. Chrome has switched the older V2 off.

A website asks for nothing and gets its own page. An extension asks for powers in its manifest, and you grant them when you install it.

03Its code runs in two places

The powers it asks for live in two different places, so its code is split between them.

The runs inside the web page. Chrome puts it into every page the manifest matches, and it can read and change that page's : hide an element, add a button, restyle the lot. It does so from an "isolated world": it sees the same elements as the page's own scripts, but neither can see the other's variables, so a hostile page can't tamper with the extension's code. Inside the page it gets only a few of the browser's extension APIs.

The runs in the browser, outside every page. It has no DOM, but it holds the powerful chrome.* APIs: the tabs, the toolbar icon, storage, screenshots, the rules for blocking requests. Chrome stops it after 30 seconds with nothing to do and starts it again when something happens, so it can't keep anything in a variable; what it must remember goes into chrome.storage.

Around them sit the extension's own pages: the popup under its toolbar icon, which exists only while it is open, and sometimes a side panel that stays.

The content script can touch the page but not the browser. The worker can touch the browser but not the page.

04A feature that needs both sends a message

The halves can't call each other's functions, so they talk the way two programs do: one sends a message, as data, and the other listens for it (chrome.runtime.sendMessage and onMessage). If you build for the web, you know this shape already. It is a page calling an API route on its own server: two places, JSON in between.

That gives a rule for reading any extension. Ask of each feature: does it need to see or change the page? That is the content script. Does it need something only the browser controls? That is the worker. If it needs both, a message crosses.

Fig 01
Content scriptIn the page
Finds the login form, then fills it
Service workerIn the browser
Keeps the vault, and decides which login this site gets
MessagesTwo: a form is here; here is its login
Five kinds of extension, each split into its two halves. Ink does work; dots are a half the feature doesn't need.

A full-page screenshot shows the rule at its busiest. The worker has the camera: chrome.tabs.captureVisibleTab() returns what is on screen, and nothing a content script can call does that. But the worker doesn't know how tall the page is, or how to scroll it. So the two take turns, one screenful at a time.

Fig 02
Round 1 of 7
Service worker

“How tall is the page?”

Content script

“4,200 px, and the window shows 900”

Page
0 of 5
A full-page screenshot of a 4,200 px page in a 900 px window. Press through the rounds.

A password manager shows why the split is also a safeguard. The content script finds the login form and says so. The worker, which keeps the vault, sends back only the one login for this site, and the content script fills it in. The vault never enters the page.

The content script is the extension's hands in one page. The worker holds the browser's keys. Messages carry the work between them.

05Driving the browser takes DevTools' channel

An AI agent that works in your browser, such as the Claude extension for Chrome, has the usual pieces: a side panel to talk to it in, a worker that opens tabs and keeps them in a tab group of their own. To use a page as you do, though, the two halves aren't enough.

A content script can click a button by calling button.click(). The page receives a synthetic click, marked isTrusted: false, which a site can tell apart and some ignore. And a content script can't see the page's console errors or its failed requests, which is most of what debugging needs.

So the agent asks for one permission most extensions never do: debugger. It lets an extension use the , the channel the DevTools panel itself uses: JSON commands such as Input.dispatchMouseEvent and Page.captureScreenshot sent to a tab, and events such as Runtime.consoleAPICalled sent back. A click sent this way goes through Chrome's real input, and the page can't tell it from yours. Chrome shows a bar saying the extension "started debugging this browser" for as long as it is attached.

Fig 03
Sends
button.click()
The page sees
event.isTrusted === false
Console errors
Can't see it
Network
Can't see it
Chrome shows
Can't see it
The same click sent two ways. The debugger permission brings the console and the network with it.

This is the same channel a browsing agent's harness uses to read a page and act on it (The page reaches the model as text), and the same one testing tools use. What differs between them is who sends the commands, and to whose browser.

Fig 04
Sender
The model's tool calls, through the extension
Wire
chrome.debugger, from the extension
Speaks
Chrome DevTools Protocol
Browser
Your Chrome
Window
On screen, in its own tab group
Logins
Yours
Three drivers on the same protocol. What changes is who sends, down which wire, to whose browser.

With debugger, an extension stops being a page script and a worker, and becomes DevTools that nobody is watching.

Connected

  • Note · same thing
    A browsing agent writes its script as it goes

    The Chrome DevTools Protocol and the accessibility snapshot, there as the way an agent's harness reads a page. Here, the extension that carries those commands into your own browser.

  • Note · same pattern
    Where code runs decides what it can reach

    Where code runs decides what it may reach: there a server and a browser, here a script in the page and a worker in the browser, with messages between.

  • Note · same thing
    Bun is one file that does every job

    Its browsers thread is the drawing half and the code half of each browser, which the V8 and WebKit thread here leans on.

  • Note · same pattern
    Where Claude runs decides what it can see

    The same harness in different places, each seeing what its place allows. Claude in Chrome is one more of those places: your own browser.

Side threads

Questions that came up once the main idea began to hold, kept as they were asked. Each opens beside the paragraph it grew from.

Terms

Checked

What the chat had wrong or only half right, against sources. The note above already says the corrected thing.

  1. Only the service worker can take a screenshot of a tab or set the badge.Any of the extension's own pages can, its popup and side panel included. What can't is the content script.Chrome for Developers, Content scripts
  2. Under Manifest V3's declarativeNetRequest, an ad blocker never sees your traffic.Its rules don't show it any, but an extension with host permissions can still watch requests through chrome.webRequest. Blocking them from code is what went, apart from extensions installed by policy.Chrome for Developers, chrome.webRequest
  3. Claude in Chrome sees a page mostly as screenshots.It also reads the page as text: its accessibility tree, its words and the console. Screenshots are for when it has to see.Claude Code docs, Use Claude Code with Chrome
  4. Playwright, as Claude Code usually uses it, runs headless in an empty profile.Playwright's MCP server opens a window by default and keeps a profile of its own between runs. chromium.launch() in a script is the headless one with an empty profile.Playwright MCP, README
  5. WebKit is Safari's drawing half, beside its JavaScript engine.WebKit is the whole engine project, JavaScriptCore included; the drawing part is WebCore. In everyday use the name means the drawing half.WebKit
  6. It isn't clear whether any browser ships its own engine on iOS.Apple has allowed it in the EU since iOS 17.4 (2024), and Japan's law opened it in 2025. No major browser had shipped one by early 2026, by public reports.Apple, Using alternative browser engines in the European Union
  7. Playwright's click is two mouse events, pressed and released.Three: it moves the mouse to the point first, then presses and releases.Playwright, Mouse.click
  8. Start your Chrome with --remote-debugging-port=9222 and Playwright can attach to it.Since Chrome 136 the switch is ignored on the everyday profile, because malware used it to read cookies. It needs --user-data-dir set to another folder, without your logins.Chrome for Developers, Changes to remote debugging switches