[Bug]: browser open hijacks a tab in the user's own window (navigates it to about:blank) after the owned container is closed
- Dominant language
- JavaScript
- Stars
- 29.3k
- Forks
- 2.9k
- Avg merge
- 15h 36m
- Merged PRs (30d)
- 70
Description
## Summary
When the interactive owned container window no longer exists (e.g. its last tab was closed and the window went away with it), `opencli browser open ` in background mode can resolve its tab into **the user's own, unrelated Chrome window** and navigate that window's first tab to `about:blank` — silently destroying whatever page the user had open. No new owned window is created in this path.
## Observed behavior
Environment: Windows 11 Pro, Chrome 151.0.7922.138, CLI 1.8.6 (`@jackwener/opencli`), Browser Bridge extension 1.0.22, `OPENCLI_WINDOW=background`.
1. An interactive owned container window existed from previous `opencli browser open …` runs (orange "OpenCLI Browser" tab group).
2. The container window was closed (its last tab closed → window gone).
3. The user had exactly one normal Chrome window open, showing a GitHub repository page.
4. Ran: `opencli browser dy-sync-bgtest2 open https://creator.douyin.com/creator-micro/content/manage` (background via env). The command returned success.
5. Immediately after, `opencli browser dy-sync-bgtest2 get url` returned `about:blank`.
6. End state: the user's own window — previously showing GitHub — now showed `about:blank`. The tab's page was lost. No new window had been created anywhere.
So a background automation run silently navigated a user-owned tab in a user-owned window, instead of creating a fresh owned window or failing.
## Root-cause analysis (extension 1.0.22, `dist/background.js`)
The page-acquisition path reuses the *first tab of whatever window was resolved as the session scope* when the existing session is not recorded as owned:
```js
const tabs = await chrome.tabs.query({ windowId: scopedWindowId });
const reuseTab = existingSession?.owned ? void 0 : tabs.find((t) => t.id);
if (reuseTab?.id) {
await chrome.tabs.update(reuseTab.id, { url: BLANK_PAGE }); // ← navigates the user's tab
await new Promise((resolve) => setTimeout(resolve, 300));
try {
const updated = await chrome.tabs.get(reuseTab.id);
if (isDebuggableUrl(updated.url)) return { tabId: reuseTab.id, tab: updated };
...
```
`scopedWindowId` is trusted as "the owned container", but ownership is never actually verified — the same gap #2202 documents for the reconcile path:
- `reconcileTargetLeaseRegistry()` only checks `chrome.windows.get(windowId)` ("does this id still exist"), never that the window is still an OpenCLI container.
- `collectOwnedGroupCandidates("interactive")` adopts any window containing a tab group titled `"OpenCLI Browser"`. With Chrome's saved-tab-groups feature, such a group can re-materialize inside the user's own window after the original container was closed (see also #2312, where these groups persist beyond their session).
Once a user window is resolved as the scope, the `reuseTab` path above navigates its first tab to `about:blank` — which matches the observed end state exactly, including `open` reporting success while the session page was a hijacked blank tab.
I don't have a deterministic minimal repro — the trigger depends on registry/group state after the container disappears — but the destructive primitive (`chrome.tabs.update(reuseTab.id, { url: BLANK_PAGE })` against an unverified window) is unambiguous in the code, and the state sequence above produced it.
## Why this is severe
Silent data loss in the user's personal browser. An automation command running with `windowMode: "background"` — i.e. explicitly promising not to disturb the user — can destroy the page the user is actively reading or filling out. Focus stealing (#2167) and orphaned blank windows (#2202) are annoyances; this one deletes user state.
## Suggested fixes
1. **Verify ownership before adopting or reusing.** Before `tabs.update(reuseTab.id, …)`, and before accepting a candidate from `collectOwnedGroupCandidates`, confirm the window is the recorded owned container — e.g. require the candidate tab to belong to the owned group inside the canonical `ownedContainers[role].windowId`, not merely "some window containing a group with the same title".
2. **Never navigate tabs outside the owned container.** If ownership cannot be established, fall back to creating a fresh owned window (the existing last-resort path) instead of borrowing a tab in a foreign window.
3. When reusing a tab, prefer one already at `about:blank` / New Tab rather than `tabs.find((t) => t.id)` (the window's first tab, whatever it contains).
## Environment
- OpenCLI CLI: 1.8.6
- Browser Bridge extension: 1.0.22
- OS: Windows 11 Pro (10.0.26200)
- Chrome: 151.0.7922.138
- Node.js: 24.14.0
Related: #2167 (background window steals focus on creation), #2202 (owned container windows never reclaimed; ownership not verified on reconcile), #2312 (interactive tab group cleanup).
Contributor guide
Research direction
Start in dist/background.js at the page-acquisition reuseTab path, then trace reconcileTargetLeaseRegistry() and collectOwnedGroupCandidates("interactive"). Use the reported background-mode open sequence if the registry and saved-tab-group state can be reproduced. Done means an unverified or foreign window is never navigated, and the command creates a fresh owned window or fails safely.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- javascript
- Domain
- cli, tooling
- Issue type
- Bug
- Difficulty
- 4/5
- Estimated time
- 3-5 days
- Activity status
- Active
- Clarity
- Mostly clear
- Newbie friendliness
- 55/100