# Another extension's iframe can lock your chrome.debugger agent out of a tab

> Chrome refuses a debugger attach to another extension's chrome-extension:// frame. If your CDP agent auto-attaches to every iframe, skip those targets first.

- Author: Chad Priest
- Published: 2026-10-01
- Canonical URL: https://blog.vodou.ai/another-extensions-iframe-can-lock-your-chrome-debugger-agent-out-of-a-tab/
- Tags: chrome, browserautomation, debugging, ai

---

If your agent is a Chrome extension that drives tabs through `chrome.debugger`, it shares every tab with every other extension the person has installed. Password managers and shopping add-ons put their own `chrome-extension://` frames into ordinary web pages, and Chrome will not let one extension debug another extension's frame. When one of those frames is in the tab, your attach gets refused.

That's the scope: extensions that use `chrome.debugger`. I first wrote that this also hits Playwright and Puppeteer over `--remote-debugging-port`. I never ran that case. The check compares the frame's extension against the extension that's calling, and a client on a debugging port isn't an extension, so it has nothing to compare. I can't show it failing there, so I'm not claiming it.

On 2026-09-29 my agent was checking an Amazon order status in my own Chrome, and it died at the first step with this:

```
Cannot access a chrome-extension:// URL of different extension (ATTACH)
```

`(ATTACH)` is my own label. My bridge tags any error from `chrome.debugger.attach` with it. The rest is Chrome's message. So Chrome refused the attach call itself. My agent never got as far as clicking anything.

The two-minute check: in your extension, find the `chrome.debugger.attach` callback and the `Target.attachedToTarget` handler. If the callback doesn't recognize this error string, and the handler never looks at `targetInfo.url`, you're exposed in both places.

## The error said "extension," and I read "Amazon

I blamed Amazon first. I was wrong, and the error had already told me so. It doesn't say the page is restricted. It says *different extension*. Something on that tab was a `chrome-extension://` URL belonging to an extension that wasn't mine. Amazon pages don't serve those. Other extensions inject them.

The next step should have been to log which frame it was. I didn't. The comment I left next to the fix lists 1Password, Honey and Amazon Assistant, which tells you I was guessing from what I had installed. The repro below does the step I skipped: it prints every child target's `type` and `url` as it arrives, and a `chrome-extension://<id>/` URL gives you the ID to match against `chrome://extensions`.

This is the general class: **auto-attach hands you targets you don't own, and Chrome holds you responsible for every one of them.**

## Two failures from `Target.setAutoAttach`, and a fix for each

My agent attaches with `Target.setAutoAttach({ autoAttach: true, waitForDebuggerOnStart: false, flatten: true })` so cross-origin iframes, like a booking dialog, show up as child sessions it can work inside. That gives you two ways to run into someone else's frame.

**The frame is already there when you attach.** That's the failure I actually saw. No event-handler filter helps, because Chrome refuses before any event fires. All you can do is recognize the error and tell the person what to do:

```js
if (/chrome-extension:\/\/ URL of different extension/i.test(msg)) {
  reject(err('OTHER_EXT', 'another Chrome extension is on this page (password manager or shopping add-on). Open the site in a normal https tab, or turn that extension off for this window, then retry'));
}
```

**The frame shows up after you attach.** An inline password menu appears when someone focuses a username field, and that's exactly when an agent is typing into a login form. In flatten mode, Chrome has already attached to the child by the time `Target.attachedToTarget` fires. So returning early doesn't undo the attach. It stops what my handler did next, which was send `Target.setAutoAttach` and `Accessibility.enable` into the child's `sessionId`. I can't tell you which of those two Chrome objected to, because both ended in `.catch(() => {})`. I threw away the evidence. Here's the handler now, with the predicate:

```js
function isExtUrl(u) { return /^chrome(-extension)?:/i.test(String(u || '')); }

if (method === 'Target.attachedToTarget' && params?.targetInfo?.type === 'iframe') {
  if (isExtUrl(params.targetInfo.url)) return;   // send nothing into it
  // ...register the child session, enable domains
}
```

That predicate is stricter than it needs to be. It skips every `chrome://` and `chrome-extension://` frame, mine included. That's fine for me because my bridge never puts its own frames into pages. If yours does, exempt your own ID, which is `chrome.runtime.id`:

```js
const OWN = `chrome-extension://${chrome.runtime.id}/`;
const isForeignExt = (u = '') => u.startsWith('chrome-extension://') && !u.startsWith(OWN);
```

The bare `return` is only correct because I use `waitForDebuggerOnStart: false`. With `true`, Chrome pauses every new child until a debugger lets it run. If you ignore a child, it stays paused, and the person's password menu freezes. In that mode, send `Target.detachFromTarget({ sessionId })` on the parent session instead of returning.

## Where the UiPath answer and the Chromanche patch stop

The [UiPath forum thread](https://forum.uipath.com/t/attaching-to-chromium-api-protocol-failed-to-use-chromium-api-on-this-page-please-close-any-extension-pop-up-using-a-different-input-method/423867) gets the cause right (a Kaspersky iframe, and a hard check in Chromium), then tells you to disable the extension. An agent running on someone else's machine can't do that. [Chromanche's commit](https://github.com/marcobazzani/Chromanche/commit/741808ed851e7af46d161783dfc2d5c203d20ea1) falls back to coordinate clicks when the focus step hits the error. That's a sensible patch, but it handles the symptom at one call site. The frame still gets followed, and the next command can fail the same way.

## Every extension the user installed shares your tab

**An agent that auto-attaches to child targets must not send anything into a target whose URL is `chrome-extension://` and not its own, and must treat a refused attach as "someone else is here," not as a crash.** You can check that rule against any codebase. It either holds or it doesn't.

To see it on your own Chrome, use a scratch profile so you don't touch your real one. Since Chrome 136 you couldn't point `--remote-debugging-port` at your default profile anyway. Launch Chrome with `--user-data-dir=/tmp/attach-repro`, install a password manager from the Web Store, and sign in to it so it has a saved login. Then save these two files in a folder and load it with Load unpacked at `chrome://extensions` (turn on Developer mode first).

`manifest.json`:

```json
{
  "manifest_version": 3,
  "name": "attach-repro",
  "version": "1",
  "permissions": ["debugger"],
  "background": { "service_worker": "background.js" },
  "action": {}
}
```

`background.js`:

```js
const OWN = `chrome-extension://${chrome.runtime.id}/`;
const isForeignExt = (u = '') => u.startsWith('chrome-extension://') && !u.startsWith(OWN);
const SKIP = false; // flip to true to test the fix

chrome.action.onClicked.addListener((tab) => {
  const t = { tabId: tab.id };
  chrome.debugger.attach(t, '1.3', () => {
    if (chrome.runtime.lastError) return console.log('attach failed:', chrome.runtime.lastError.message);
    console.log('attached', tab.url);
    chrome.debugger.sendCommand(t, 'Target.setAutoAttach',
      { autoAttach: true, waitForDebuggerOnStart: true, flatten: true });
  });
});

chrome.debugger.onEvent.addListener((src, method, p) => {
  if (method !== 'Target.attachedToTarget') return;
  console.log('child', p.targetInfo.type, p.targetInfo.url);
  if (SKIP && isForeignExt(p.targetInfo.url)) {
    console.log('skipped', p.targetInfo.url);
    return chrome.debugger.sendCommand(src, 'Target.detachFromTarget', { sessionId: p.sessionId });
  }
  const child = { tabId: src.tabId, sessionId: p.sessionId };
  chrome.debugger.sendCommand(child, 'Runtime.enable', {}, () =>
    console.log('Runtime.enable', chrome.runtime.lastError?.message || 'ok'));
  chrome.debugger.sendCommand(child, 'Runtime.runIfWaitingForDebugger');
});

chrome.debugger.onDetach.addListener((src, reason) => console.log('detached', reason));
```

Open the repro's service worker console from its card on `chrome://extensions`. Then run it twice on a sign-in page for a site the password manager has a login saved for.

1. **Frame already present.** Click into the username field and wait until the password manager's inline menu is visible. Then click the repro's toolbar icon. `attach failed: Cannot access a chrome-extension:// URL of different extension` means you've reproduced what I hit. `attached <url>` means your build allowed it.
2. **Frame arrives later.** Reload the page, click the toolbar icon first, then click into the username field and wait for the menu. Look for a `child iframe chrome-extension://<id>/...` line. What comes after it is your answer: a `Runtime.enable` line with an error, or a `detached <reason>` line, is the failure. Note the reason string, because that's the one fact I didn't keep. With `SKIP = true`, a pass is a `skipped chrome-extension://...` line and no `detached` line after it.

If neither run prints any `chrome-extension://` line, the menu never injected. That isn't a pass. Focus the field again, wait for the menu to appear, and rerun.

---

Source: [Another extension's iframe can lock your chrome.debugger agent out of a tab](https://blog.vodou.ai/another-extensions-iframe-can-lock-your-chrome-debugger-agent-out-of-a-tab/) by Chad Priest, from Building Vodou in Public.
