Why a Chrome extension technically can't read your ChatGPT, Claude or Gemini chats
"This extension can read everything you type into ChatGPT" is a reasonable worry, because a lot of extensions genuinely can. The Manifest V3 permission model gives every installed extension a declared, inspectable surface, though — and that surface either includes "read this page's content and send it somewhere" or it doesn't. Below is what that surface looks like in practice, line by line, using DevPause's own manifest.json and content script as the example, and how to check the same thing on any extension you have installed.
Permissions are declared, not implied
A Chrome extension cannot touch anything it did not declare in manifest.json. There is no "also quietly read the DOM" permission — every capability is one of a fixed list Chrome shows the user before install. Here is the full permissions block from DevPause's manifest:
"permissions": [
"storage",
"alarms",
"notifications"
],
"host_permissions": [
"https://devpause.vercel.app/*"
]
storage is local settings (which buttons are on, chosen language). alarms and notifications are the "answer ready" ping when you switch tabs. host_permissions is the important one: it is the list of origins the extension's background service worker is allowed to send a network request to, and it contains exactly one URL — the extension's own server. chatgpt.com, claude.ai and gemini.google.com are not in that list, which means the background worker has no permission to fetch anything from those domains even if its code tried to.
Content scripts are matched by URL, and that match is the only door
The code that actually runs on the chat pages is declared separately, under content_scripts:
"content_scripts": [{
"matches": [
"https://chatgpt.com/*",
"https://claude.ai/*",
"https://gemini.google.com/*"
],
"js": ["src/plain.js", "src/content.js", "src/insert.js", "src/tools.js"],
"run_at": "document_idle",
"all_frames": false,
"css": ["src/tools.css"]
}]
Three things worth noticing: the match list is exactly the three sites the extension supports, not <all_urls>; all_frames is false, so embedded iframes on those pages are not touched; and this is a declaration, not a runtime decision — Chrome itself enforces that this code cannot be injected anywhere else, independent of what the code says about itself.
A content script cannot phone home on its own
This is the part people usually miss: even a content script that runs on the right page cannot just fetch() an arbitrary URL. It inherits the page's Content-Security-Policy for same-context requests, and for anything that goes through the extension's own messaging, DevPause's CSP pins the connection:
"content_security_policy": {
"extension_pages": "script-src 'self'; object-src 'none'; base-uri 'none'; connect-src https://devpause.vercel.app"
}
connect-src https://devpause.vercel.app means the extension's own pages and service worker can only ever open a network connection to that one origin. There is no code path — not a bug fix, not a future version, not a compromised dependency — that lets a fetch() from inside this extension reach a different server, short of Chrome shipping a new version with a different manifest and the user accepting a new permission prompt for it.
What the content script is actually allowed to look at
Given that network path, the next question is what the content script reads from the page at all. The relevant function is four lines:
function generating() {
try {
return selectors.some((s) => document.querySelector(s) !== null);
} catch {
return false;
}
}
That is the only per-tick read of the page: does an element matching a "Stop generating" button selector exist, yes or no. It does not call .textContent, .innerText, or walk the message list. The one place DevPause does read an answer's text is the "Copy clean" button, and that only runs inside the click handler the user just triggered — there is no polling loop, timer, or background listener that reads message content without a click.
The UI itself can't be read from the page either
The ad card DevPause shows next to an in-progress answer is attached through a closed shadow root:
const root = host.attachShadow({ mode: "closed" });
mode: "closed" means the host page's own JavaScript cannot call element.shadowRoot and reach inside this node, and the extension's code inside that shadow tree cannot see or query anything outside it either. It is a two-way seal: the chat page can't inspect the extension's UI, and the extension's UI can't inspect the chat page beyond the one querySelector check above.
So what does leave the browser, concretely
None of the above is an argument that nothing leaves the browser — some data does, for ad delivery and basic fraud protection, and it is worth listing exactly what, rather than asserting "we respect your privacy" and leaving it there:
| Sent to the server | Why |
|---|---|
| Random install id + signature (not linked to your account on any site) | Count impressions, block fraud |
| Country and language chosen in the extension popup | Match an ad |
| Extension version | Compatibility |
| Ad id, seconds shown, clicked or not, which of the three sites | Advertiser report |
| Invite code from the install link, if any | Credit a referral |
Conversation text, message titles, and page URLs are not on that list, and the manifest above is the reason they structurally cannot be: there is no permission to read them outside a click, and no destination to send them to even if there were.
How to check this yourself, on any extension
You do not need to take any extension's word for this:
- Open
chrome://extensions, enable Developer mode, and click "Details" on the extension — the permissions it actually requested are listed there. - From the same Details page, open the packed extension's files (or unpack the
.crx) and readmanifest.jsondirectly — it is plain JSON, not obfuscated. - Open DevTools on chatgpt.com, claude.ai or gemini.google.com, go to the Network tab, and watch while an extension is active: every request's initiator and destination domain are visible, so a request to an unexpected server stands out immediately.
- If the extension ships unminified source (DevPause's
src/files are readable, not bundled into one blob), read the content script itself — the four-linegenerating()function above is the entire "reads the page" surface.
Frequently asked questions
Could a future update quietly add permission to read chat content?
Not silently: Chrome re-reviews an extension and re-prompts the user for consent whenever an update requests new host permissions or broadens existing ones. A jump from one origin in host_permissions to something wider would not install itself unnoticed.
Does "Copy clean" send the copied text anywhere?
No — it writes to the operating system clipboard via the browser's clipboard API, which has no path to the extension's connect-src origin. The text goes from the page to your clipboard and nowhere else.
Why trust a privacy policy at all if the manifest already proves this?
The manifest is the enforceable part; the privacy policy is the plain-language description of the same facts, including the small list of metadata above that the manifest's one permitted origin does receive. Reading both is more convincing than either alone.
DevPause is a free Chrome extension that adds utility buttons under ChatGPT, Claude and Gemini answers — clean copy, shorter, simpler, as a table, continue, and a wide-screen toggle. It is ad-supported (one small text line while an answer streams) rather than subscription-based, and the manifest and content script discussed above are the real, current ones it ships with.