DEENESH

NOTE–015 · Security tooling

Reporting AI exposure from declared extension permissions

ExtScope answers one narrow question: which of my extensions can reach my AI chats? The engineering was mostly about deciding what it should refuse to claim.

Any Chrome extension with the right host permission can read and modify the page you are typing into. That includes the tab where you paste code, contracts, or customer data into an AI assistant. I built ExtScope to make that exposure visible. This article covers the design choices that mattered, including the ones that made the tool less dramatic.

Start with what the platform tells you

Chrome’s management API returns, for each installed extension, its declared permissions and host permissions. That is the whole input. It is static and it comes from the manifest, not from watching the extension run.

That limits what can be honestly said. A grant of <all_urls> means an extension is allowed to touch a page. It does not mean it does. A password manager and a shady coupon tool might hold the same grant for very different reasons. So the tool reports capability and exposure, and its wording never calls anything malicious. The README states this limitation near the top, not in a footnote.

Parse match patterns as data

Host permissions are Chrome match patterns: https://claude.ai/*, *://*.openai.com/*, <all_urls>, *://*/*. Matching them by substring would be wrong in both directions. So the normalizer parses each pattern into a scheme, a host, and flags for wildcard subdomain and wildcard host.

Two details shaped the code:

  • A *.example.com pattern also matches the bare example.com, but it is still a wildcard grant. The classifier keeps that distinction because it changes how the finding reads.
  • A pattern that fails to parse returns null and lands in an unparsed list. It is never quietly counted as harmless. For an audit tool, a false “safe” is worse than a noisy “unknown.”

Each pattern is classified against a versioned registry of seven services: ChatGPT (which includes openai.com), Claude, Gemini, Microsoft Copilot, Perplexity, DeepSeek, and Grok (grok.com and x.com). The registry lives in one file so the engine, the tests, and the interface cannot drift apart. Users can add custom services in settings, and those merge over the defaults.

Levels you can predict from a manifest

I avoided a numeric risk score. A score of 73 asks the reader to trust an opaque formula, which is the situation the tool exists to get people out of. The levels are ordered rules:

<all_urls> + scripting or a sensitive permission   → Extensive
<all_urls> or a wildcard http(s) host               → Broad
no matching AI service                              → None detected
more than one service                               → Broad
exactly one service                                 → Limited

One refinement: a single service becomes Broad when the extension also holds scripting and a sensitive permission. Order matters. A broad host grant dominates everything else because it alone exposes every supported service. “Sensitive” permissions include clipboard access, cookies, history, downloads, native messaging, and the debugger. Page-level capabilities such as scripting and activeTab get their own findings.

Each finding cites the permission or pattern behind it. The label is a summary. The evidence is the product.

Changes matter more than snapshots

An extension that was fine last month can ship an update that requests more access. The service worker stores a snapshot after every scan and diffs it against the previous one. Install, uninstall, enable, and disable events trigger a rescan automatically.

Not every difference deserves attention. A version bump is recorded but is not an alert. The relevant set is narrow: added host access, an added sensitive permission, or an increased exposure level. Only those put an extension into a Changed state on the dashboard. The overlay does not overwrite the computed level. The underlying findings stay visible so the reader can see what the extension is and why it changed.

Treat every extension name as hostile

The interface shows names and descriptions written by third-party developers. React’s default escaping prevents markup injection, but it does not stop Unicode bidirectional-override characters from visually reordering a name to imitate a trusted one, and that would be a bad failure in a tool whose job is accurate reporting. A small sanitizer strips control and bidi characters before display and before export. Exported reports are the same trust boundary as the screen.

Earn the privacy claim

A tool that audits privacy has to be the least interesting extension you own. ExtScope requests two permissions, management and storage, and no host permissions. It therefore cannot read AI pages even if it wanted to.

I first assumed opening Chrome’s extension page needed the tabs permission. It doesn’t, provided the code never reads a tab’s URL or title. I checked in a real browser and removed the permission. The Content Security Policy limits connect-src to the extension’s own origin, so even an unexpected fetch would be confined.

Then I tested the claim as a user-visible behavior. A Playwright suite loads the packaged build into real Chromium, runs through onboarding, scanning, disabling, exporting, and data deletion, and asserts that every network request stays on the extension’s own origin. A privacy statement is more useful when it can fail a build.

Keep the core boring

The rules engine, normalizer, change detector, and sanitizer import neither React nor Chrome APIs. They take plain objects and return plain objects, so unit tests use fabricated manifests and run in milliseconds. React and Vite sit on top for the popup and dashboard. I recorded the boundary in an architecture decision record, along with its cost: the service worker and the UI each touch Chrome storage directly instead of sharing a messaging layer. At this scale, I would rather have the simpler build.

What I would do next

ExtScope is at version 0.1.0 and installs from source today. A Chrome Web Store listing is drafted, but it is not published, and I have no usage data. The roadmap defers store-status monitoring, team policy files, and other browsers until real users show which findings they act on.

The main lesson was that restraint is a feature. Declared permissions can support the statement “this extension is allowed to read your Claude tab,” and ExtScope says exactly that, with the evidence attached. It can’t support “this extension is reading it,” so it doesn’t say it.