Inspect Chrome Extension Content Scripts
How to Trace an Extension’s Content Scripts
Follow a content script from its matching URLs to the exact page elements it reads or changes, then check where the data goes.
Start with the content_scripts URL rules
Get a currently available public package through CRXFile and extract its ZIP. CRXFile retrieves packages; it doesn't provide a safety verdict. Our manifest reading guide explains the other entry points. For this walkthrough, use the original minimal Manifest V3 example below.
{
"manifest_version": 3,
"name": "Content Script Trace Demo",
"version": "1.0.0",
"content_scripts": [
{
"matches": [
"https://example.com/articles/*"
],
"exclude_matches": [
"https://example.com/articles/private/*"
],
"js": [
"content.js"
],
"run_at": "document_idle",
"all_frames": false,
"world": "ISOLATED"
}
]
}Read matches together with exclude_matches. The exclusion removes private article paths from this rule. These four URLs make the boundary easier to check:
| URL | Eligible under this rule? |
|---|---|
| https://example.com/articles/intro | Yes, subject to Chrome's site-access controls. |
| https://example.com/articles/private/draft | No, explicitly excluded. |
| https://docs.example.com/articles/intro | No, different host. |
| http://example.com/articles/intro | No, different scheme. |
Content script paths matter. This differs from host_permissions, where paths are ignored. Check Chrome's match pattern syntax and content script manifest reference before treating a domain list as an injection rule.
Find the files, timing, and frame
Open content.js relative to the extension's root folder. If the js array lists several files, preserve that order while reading: later files may depend on earlier ones. Check listed CSS files too. A changed page can come from a stylesheet without any DOM-writing JavaScript.
Here, run_at: document_idle means the DOM is complete when injection occurs, and all_frames: false targets the top frame. Neither setting promises that an app has finished adding its own content. If frames are included in another extension, check each frame's URL against the rules instead of assuming the parent page authorizes everything inside it.
Trace a content script's DOM operations
This is the complete content.js for our example. Read it as a short sequence of reads and writes:
(() => {
const headings = document.querySelectorAll("article h2");
for (const heading of headings) {
if (heading.querySelector("[data-crxfile-note]")) continue;
const note = document.createElement("span");
note.dataset.crxfileNote = "true";
note.textContent = " [reviewed]";
heading.append(note);
}
})();querySelectorAll("article h2") selects second-level headings inside article elements. The inner selector skips a heading that already contains our marker. Then the script creates a span, gives it a data attribute, writes a fixed text label, and appends it to the heading. It doesn't read the heading's text or send a request.
That distinction matters in a review. A nearby textContent assignment could insert a label, while reading heading.textContent could collect page content. Follow which side of the assignment the page data appears on. Search for innerHTML and inspect where its input comes from, too.
The manifest explicitly uses ISOLATED. A content script in that world has a separate JavaScript environment from the page, but can still change their shared DOM. Chrome's content script guide explains why isolation doesn't make page elements private.
Check what happens when the page changes
Open our interactive DOM check. Two article headings receive a [reviewed] marker, and the heading outside the article stays unchanged. Click “Add article heading.” The new heading has no marker until you click “Run content.js again.” Running it once more keeps one marker per heading.
We checked those outcomes in Chrome 155.0.8059.85 on October 23, 2026. The check page runs the same file as an ordinary page script. It tests DOM behavior, not extension installation, URL matching, or isolated-world execution. In an extension, this one-time selection would still miss headings added later unless something runs the code again or watches for changes.
Look for injection outside the manifest
Search the extracted source for registerContentScripts and executeScript. The first can register scripts with rules chosen at runtime; the second can inject files or a function when an event fires. Follow the caller back to its trigger, such as an action handler, and check its target tab and frame. An aliased API call or minified bundle can escape a literal text search.
Chrome's scripting API reference covers those paths. An empty content_scripts array doesn't prove an extension never touches pages. Our host permissions vs activeTab comparison helps distinguish ongoing site grants from access that follows a user invocation.
Use DevTools when a script seems missing
For a trusted extension you already run, open the target page's DevTools, find its content script under Sources, and place a breakpoint at the first DOM operation. Select the extension's execution context in Console when inspecting its variables. Chrome's extension debugging tutorial shows how to locate the different components.
Check the actual URL, exclusions, and current site-access setting before changing permissions. Reload the extension after editing its files, then reload the test page so its scripts can run again. For a missing heading, also check when it entered the DOM and whether it lives inside a frame. A broader host pattern won't fix a selector aimed at the wrong element.
Follow messages beyond the page
In a larger package, a content script may pass selected text through chrome.runtime.sendMessage or a long-lived connection. Find the receiving listener and follow what it does with that payload. A clean-looking content script can still hand data to a worker that stores it or sends it to a server. Chrome's message passing guide describes the connection between those components. Our MV3 service worker walkthrough follows a message from its sender through the receiving handler to local storage.
Keep a review note with the matching URL, script filename, DOM read or write, and destination for any collected data. Use our source inspection workflow to follow the remaining entry points. If the trail ends in a bundled helper, record that filename and the unresolved call so the next reviewer can continue from the same place.