The Errors
When building Tuneo's audio capture feature, I encountered two errors depending on the scenario:
Error: Extension has not been invoked for the current page
(see activeTab permission).Error: Chrome pages cannot be captured.
The first fires on normal web pages when tabCapture is called before the user has interacted with the extension on that tab. The second fires when the user is on achrome:// or chrome-extension:// page, which are entirely off-limits for capture. Both errors would crash the capture flow silently, leaving the user wondering why the listen button did nothing.
Why the activeTab Error Happens
The activeTab permission is special in Chrome Extensions. It does not grant persistent host access to any URL. Instead, it grants temporary access to the currently active tab, but only afterthe user invokes the extension on that tab — by clicking the toolbar icon, using a context menu item, or pressing a keyboard shortcut defined in the manifest.
If tabCapture.capture()is called before this invocation happens (e.g., the popup opened and auto-started listening), Chrome rejects it because the extension hasn't been “activated” for that page yet. The error message explicitly points to activeTab as the culprit.
// Popup opens → auto-captures without user clicking the icon
// ❌ "Extension has not been invoked for the current page"
useEffect(() => {
// This runs when popup opens, not when user clicks the extension
startCapture() // will fail
}, [])Why the Chrome Pages Error Happens
Chrome intentionally restricts tabCapture from working on internal Chrome pages (chrome://, chrome-extension://,about://, chrome://newtab, etc.). This is a security boundary — these pages are part of the browser UI itself and capturing them could expose sensitive information like passwords, browsing data, or extension internals. There is no workaround or permission to bypass this.
The Solution
I solved both issues with a combination of URL pre-checks and manifest changes.
1. Block chrome:// pages before capture
Before initiating capture, check the tab's URL. If it starts with a blocked scheme, show a clear user-facing message instead of failing silently:
function canCaptureTab(url) {
if (!url) return false
const blocked = ['chrome://', 'chrome-extension://', 'about:', 'chrome://newtab/']
return !blocked.some((prefix) => url.startsWith(prefix))
}
const tab = await chrome.tabs.get(tabId)
if (!canCaptureTab(tab.url)) {
showError("Can't capture Chrome system pages.")
return
}2. Switch from activeTab to explicit host_permissions
The activeTab permission is great for minimal-privilege extensions, but for an extension that needs to capture any tab the user navigates to, it is too limiting. I added explicit host_permissions to the manifest to cover all URLs:
"permissions": [ "tabCapture", "activeTab", "storage", "sidePanel", "offscreen", "scripting" ], "host_permissions": [ "<all_urls>" ]
With <all_urls> in host_permissions, the extension has persistent host access to every URL the user visits. Combined withtabCapture, capture can start immediately when the user clicks the listen button, without needing a prior extension invocation. TheactiveTab permission is kept for other use cases but is no longer the sole source of host access.
3. Require user action to start capture
Even with host_permissions, I made sure capture only starts when the user explicitly clicks the listen button. This avoids the “auto-capture on popup open” problem and gives the user full control over when audio is recorded:
// Only start capture on explicit user click
const handleListen = async () => {
setStatus('listening')
chrome.runtime.sendMessage({ action: 'capture' })
}
return <button onClick={handleListen}>♪ Listen</button>Key Takeaways
activeTabonly grants temporary host access after extension invocation — usehost_permissionsif your extension needs it.chrome://and internal pages are never capturable. Check the URL upfront and handle it gracefully.- Always start capture on explicit user action, not automatically on popup open.
- Use descriptive error messages so the user knows why capture failed, not just that it failed.