Skip to main content
A browser extension can run code inside a web page in three ways. A manifest content_scripts entry runs on every page that matches a pattern. chrome.scripting.executeScript runs one script in one tab when you call it. chrome.scripting.registerContentScripts registers a content script at runtime and keeps it registered. This page explains when each one runs, which permission it needs, and how the browsers differ. It also lists the console lines that injection failures print and where the file goes in an Extension.js project.

The three ways to run code in a page

Manifest content_scripts. A static declaration in manifest.json. The browser injects the listed js and css files into every page that matches matches, at the time set by run_at. Extension.js compiles each entry and wraps it for HMR, see Content scripts. chrome.scripting.executeScript. A one-off call from the service worker or another extension page. It targets one tab and runs either files (paths inside the extension) or func (a function serialized into the page, with optional args). It needs the scripting permission plus access to the tab, from activeTab after a user gesture or from a matching host_permissions pattern. chrome.scripting.registerContentScripts. A dynamic registration with the same shape as a manifest entry (id, matches, js, css, runAt, world). The browser injects it into pages that match from that moment on, and the registration persists across browser restarts by default. It needs the scripting permission and host_permissions that cover matches, because activeTab does not apply to future pages. Use the manifest entry when the feature belongs on a known set of sites. Use executeScript when the user triggers the feature, for example from a toolbar click. Use registerContentScripts when the set of sites is decided at runtime, for example from a settings page.

Manifest snippet

The scripting permission unlocks both runtime APIs. activeTab covers the tab the user clicked in, and host_permissions covers every page that matches, which dynamic registration requires:
Chromium and Firefox both read this block as written, so it needs no browser prefix. Use a prefix only for world: "MAIN" on a manifest content script, because Firefox ignores the field. Declare it as chromium:world and keep an isolated-world fallback, as shown in browser-specific fields. The runtime calls that match this manifest:

Per-browser differences

On Safari, enabling the extension is not enough. Until you grant website access, no content script runs, and the same applies to scripts you inject at runtime. See Safari for the enable and grant steps.

Console lines you will see

Copy the line that you see into search. Each one maps to one cause. Cannot access contents of url "https://example.com/". Extension manifest must request permission to access this host. The tab is outside your host permissions and activeTab was not granted for it. Add the host to host_permissions, or call executeScript from a handler that runs after a user gesture on the tab. Could not load file: 'scripts/highlight.ts'. The executeScript or registerContentScripts call names a source path. Extension.js compiles scripts/highlight.ts to scripts/highlight.js, so inject the emitted .js path. Failed to load resource: net::ERR_FILE_NOT_FOUND A chrome-extension:// URL that ends in .ts (or another path that never reached dist/). Same fix: reference the emitted .js file, then confirm it exists under dist/<browser>/scripts/. NS_ERROR_CONTENT_BLOCKED The Firefox form of the same missing-file error on a moz-extension:// URL. Inject the emitted .js path. Cannot access a chrome:// URL Browser pages cannot be scripted. Test on a normal https:// page. The extensions gallery cannot be scripted. The Chrome Web Store is off limits to every extension. Test on another page. This page cannot be scripted due to an ExtensionsSettings policy. A managed browser blocks your extension on this host. Test on a host that the policy allows, or on a profile that the policy does not manage.

The Extension.js way

Put runtime-injected files in the scripts/ special folder. The rules are short:
  • scripts/ lives beside package.json at the project root, not inside src/. A nested src/scripts/ is a plain folder.
  • Every file there compiles to .js and lands at dist/<browser>/scripts/<name>.js.
  • Reference the emitted path in your files or js array. A .ts path builds fine but 404s in the browser.
  • When a runtime literal names a .ts source, the build prints a warning with the emitted path to use.
  • The file follows the content script contract: export default a synchronous function that returns an optional cleanup.
See Special folders for the full folder contract. During extension dev, scripts that you inject with executeScript from scripts/ are replayed on edit, so the injected code updates live the way declarative content_scripts do. See Reload and HMR. Scaffold a project that injects a scripts/ entry at runtime:
For the static path, start from the content template instead:

See also