"You'll need this maybe one feature in ten. But when you need it, no amount of clever ISOLATED-world DOM access substitutes. Lesson 5 is the four legitimate ways content scripts and pages talk, with which to pick when."
When You Actually Need a Bridge
Most ClipDeck features (read DOM, read selection, ship to SW) live entirely in the ISOLATED world. The bridge is only needed when:
- You need to read a page-defined global —
window.React.version,window.__INITIAL_STATE__, a CMS's data-layer object. - You need to call a page-defined function — "trigger the site's own search," "hand a payload to the page's analytics queue."
- You're integrating with a framework's runtime hooks (React DevTools-style work).
Everything else — DOM reads, DOM writes, event listening, storage round-trips — does not need a bridge. Reach for one only when ISOLATED genuinely cannot answer the question.
Pattern A — CustomEvent
Simplest. One world dispatches; the other listens. Works in either direction.
- Dispatched on a DOM node (usually
documentorwindow). - Payload goes in
detail, but with caveats: thedetailobject is shared, but anything richer than primitives may be wrapped/unwrapped across worlds — keep it to JSON-clonable shapes. - One-shot signaling: "the user just did X," "please refresh your sidebar."
Pattern B — window.postMessage
The DOM messaging API. Both worlds share the same window object (it's the DOM window, not a JS global), so they can post messages to it and listen on it. The message payload is structured-cloned, so plain data survives intact.
- Request/response works naturally: post a request with a unique id, listen for a response with the same id.
- Cross-cuts iframe boundaries too (with explicit origin checks).
- Sentinel pattern is mandatory — the page can post anything, so filter on a known marker field before trusting the payload.
Pattern C — Injected Script Tag (Legacy)
The pre-Chrome-111 path for getting code into the page's JS world:
- Write
injected.jsas a separate file inside the extension. - List it in
web_accessible_resourcesin the manifest so the page is allowed to load it. - From the content script, create a
<script src="chrome-extension://.../injected.js">and append it todocument.documentElement. - The script tag executes in the page's world. Communicate back via CustomEvent or postMessage.
Still works in modern Chrome, still useful when you need MAIN-world execution from a declarative content script that hasn't migrated to world: 'MAIN'.
Pattern D — chrome.scripting with world: 'MAIN' (Modern)
Lesson 3 introduced this. The SW calls chrome.scripting.executeScript({ target, world: 'MAIN', func }) and the function runs directly in the page's JS world, returning its result. No script tag, no web_accessible_resources, no relay code.
This is the cleanest path for one-shot MAIN-world calls triggered by an extension event (toolbar click, context menu, message from popup). When you're declaratively injecting and need MAIN, Chrome 111+ also lets you set "world": "MAIN" in the content_scripts manifest entry.
Sentinel and Origin — The Two Discipline Habits
Anything that listens for messages from arbitrary pages must guard:
- Sentinel field. Every message your script sends carries
{ source: "clipdeck-content" }or similar. Every listener checks that field first. A malicious page can post a fake message with the same sentinel, but the standard practice prevents most accidental cross-talk with other libraries. - Origin check. For
window.postMessage, validateevent.source === window(the message came from this same window, not an iframe) andevent.originmatches the expected URL when relevant. - Treat MAIN-world messages as untrusted. Once data crosses from MAIN to ISOLATED, it came from somewhere the page can influence. Validate types, lengths, and shapes before storing.
'*' targetOrigin trap. window.postMessage(payload, '*') sends the message to every listener, regardless of origin. Convenient during development, dangerous in production — any third-party script that registered a message listener on the same window will see your payload. For ISOLATED↔MAIN inside one tab, '*' is acceptable because the same-tab boundary already isolates you from external sites; for ANY cross-frame messaging, set the specific target origin you intend.