jackwener / jackwener/OpenCLI

[Bug]: browser open hijacks a tab in the user's own window (navigates it to about:blank) after the owned container is closed

Open
#2,364 0 comments 0 reactions 0 assignees View on GitHub
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

Open the contributing 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

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.