Running Content Scripts on file:// URLs and Special Pages
Get MV3 content scripts onto local files, about:blank and srcdoc frames, data and blob URLs: file scheme opt-in, match_origin_as_fallback, match_about_blank, and the pages no extension can reach.
Table of Contents
A Markdown viewer extension should render local .md files opened in the browser. A reading tool should work on a saved HTML page. An annotation tool should reach the comment box inside an about:blank iframe that a web app writes into with document.write. All three fail by default: content scripts declared for https://*/* do not match file:// URLs, local files need a separate user opt-in, and frames without a real URL — about:blank, srcdoc, data:, blob: — need their own matching rules. This guide covers each special case and the pages that remain unreachable no matter what. It belongs to content scripts and DOM injection.
How matching treats special URLs
Content script matching compares a frame’s URL with the matches patterns. Ordinary web URLs match https://… patterns. file:///… URLs match only patterns with the file scheme (file:///*) or <all_urls> — and even then, the browser injects only if the user has turned on “Allow access to file URLs” for the extension. Frames whose URL is about:blank or about:srcdoc inherit the origin of the document that created them but have no URL of their own to match; match_about_blank: true (older) and match_origin_as_fallback: true (Chrome 99+, broader) tell the browser to match them by their creator’s URL instead. data: and blob: frames are matched by origin only with match_origin_as_fallback. Browser pages, the store and other extensions are never matched.
Step-by-step: reach the special cases
1. Declare file access in the manifest
1{
2 "content_scripts": [{
3 "matches": ["file:///*/*.md", "file:///*/*.markdown"],
4 "js": ["markdown-viewer.js"],
5 "css": ["markdown-viewer.css"],
6 "run_at": "document_end"
7 }],
8 "host_permissions": ["file:///*"]
9}
Execution context: the manifest. A file pattern needs three slashes — file:/// — because the host part is empty. Path globs can restrict matching to particular extensions. Declaring file patterns does not show a separate install warning in Chrome, because nothing happens until the user opts in.
2. Detect and explain the file opt-in
1// popup.js
2const allowed = await chrome.extension.isAllowedFileSchemeAccess();
3if (!allowed) {
4 notice.textContent = "To open local Markdown files, enable “Allow access to file URLs” for Readable.";
5 openBtn.onclick = () => chrome.tabs.create({ url: `chrome://extensions/?id=${chrome.runtime.id}` });
6}
Execution context: the popup or options page. Extensions cannot request file access programmatically — the toggle lives on the extension’s details page. Detect it and link the user there; tabs.create can open chrome://extensions even though content scripts cannot run on it. Show this only to users who use the file feature, not to everyone.
3. Render a plain-text file safely
1// markdown-viewer.js
2const pre = document.querySelector("body > pre");
3if (pre && document.contentType !== "text/html") {
4 const source = pre.textContent;
5 const article = document.createElement("article");
6 article.append(renderMarkdown(source)); // DOM nodes, sanitised
7 document.body.replaceChildren(article);
8 document.title = source.match(/^#\s+(.+)$/m)?.[1] ?? document.title;
9}
Execution context: a content script on the file URL. Browsers display text files as a <pre> element; the script replaces it with rendered output. Local files are still untrusted input — a downloaded .md file can contain HTML — so the renderer must produce sanitised DOM nodes, not set innerHTML with raw Markdown output. See sanitising untrusted page data in an extension.
4. Match about:blank and srcdoc frames
1{
2 "content_scripts": [{
3 "matches": ["https://app.example.com/*"],
4 "js": ["editor-helper.js"],
5 "all_frames": true,
6 "match_origin_as_fallback": true
7 }]
8}
Execution context: the manifest. Rich-text editors and email clients often build their editing surface inside an about:blank or srcdoc iframe created by the page. With match_origin_as_fallback, the browser matches such frames by the origin of the document that created them — here https://app.example.com — so the script runs there too. all_frames is required, since these are subframes. Matching uses the origin only, so matches path components are ignored for these frames.
5. Inject programmatically where declarations fall short
1// sw.js — inject into all frames of a tab, including about:blank ones
2await chrome.scripting.executeScript({
3 target: { tabId, allFrames: true },
4 files: ["editor-helper.js"],
5});
Execution context: the service worker. Frames created after page load — an editor iframe that appears when the user clicks “Reply” — receive manifest content scripts automatically if they match. For frames that existed before the extension was installed, or when you need to act on a user gesture, programmatic injection with allFrames reaches every accessible frame, including about:blank ones in Chrome. See injecting into iframes and all frames.
6. Accept the pages you cannot reach
Browser settings pages, the new tab page, extension stores, other extensions’ pages, the built-in PDF viewer and view-source: pages are off-limits to every extension. Design features so their absence there is explained in the UI rather than appearing broken, as covered in handling injection errors on restricted pages.
7. Test each special case explicitly
Add end-to-end fixtures for a local file (Playwright can open file:// URLs; enable file access with a prepared profile or by toggling it in the details page in test setup), a page that writes into an about:blank iframe, and a srcdoc frame. These cases regress silently when manifest entries are refactored.
Common mistakes
file://*with two slashes. It matches nothing; file patterns needfile:///.- Expecting file access without the user toggle. It is off by default and cannot be requested by API.
- Forgetting
all_frameswithmatch_origin_as_fallback. The fallback only applies to subframes you inject into. - Rendering local files with innerHTML. Local content is still untrusted.
- Promising PDF support. The built-in viewer cannot be scripted.
Cross-browser variation
- Chrome / Edge: file access toggle per extension;
match_origin_as_fallbackfrom Chrome 99,match_about_blankfor older behaviour. - Firefox: content scripts can run on
file://pages matched byfile:///*with host permission, without a separate toggle in many versions;match_about_blanksupported;match_origin_as_fallbacksupport arrived later — feature-test. - Safari:
file://support for extensions is limited; local file features may not be viable.
Verification
- With file access off, open a local
.mdfile and confirm the popup explains the toggle. - Turn it on, reload, and confirm the file renders.
- Open a page that writes into an
about:blankiframe and confirm the content script runs inside it (console.log(location.href)in the frame showsabout:blank). - Confirm nothing is injected on
chrome://pages.
FAQ
Can I read arbitrary local files from the extension?
Only files the user opens in the browser (via content scripts with file access) or picks with a file input. There is no general file system access.
Why does my script run in the srcdoc frame but not the about:blank one?
Some about:blank frames are created before your script’s injection point or are navigated immediately; try run_at: "document_start" or programmatic injection.
Do blob: frames from other origins match?
Only by their creator’s origin, and only if your host permissions cover it.
Does the file access toggle apply to programmatic injection too?
Yes. chrome.scripting.executeScript into a file:// tab fails unless file access is on, with an error you can classify and explain the same way.
Can I watch a local file for changes and re-render?
Not directly — content scripts cannot read the file system. A “Reload” button that reloads the tab is the practical approach, or a native messaging host if live watching is essential.
Related
- Writing match patterns and globs — pattern syntax in depth.
- Injecting content scripts into dynamic iframes — frames created at runtime.
- Handling restricted URLs and tab permissions — what tabs APIs report for these pages.
- Content scripts and DOM injection — the parent topic.