Repository navigation
Sidebar tab awareness: tabs.json has empty url/title on non-localhost sites since v1.14 (manifest permission gap) #1256
Description
Summary
Since v1.14.0.0 (ed1e4be2), the live tab awareness feature ships with a manifest permission gap: the extension can't read tab.url / tab.title for any site that isn't 127.0.0.1, so tabs.json is written with empty strings and active-tab.json stops updating after the first navigation away from /welcome.
Net effect: the sidebar Claude Code REPL can't see what tab the user is actually on, and falls back to stale state from initial launch.
Repro
gstack browse connect— Chromium opens at127.0.0.1:34567/welcome- Manually navigate (URL bar or click a link) to any non-localhost page, e.g.
https://news.ycombinator.com - Read state files:
cat <repo>/.gstack/tabs.json cat <repo>/.gstack/active-tab.json
Actual
tabs.json updates timestamp + tabId, but url and title are empty:
{
"updatedAt": "2026-04-28T06:59:36.945Z",
"reason": "updated",
"tabs": [{
"tabId": 1927305561,
"url": "",
"title": "",
"active": true,
"windowId": 1927305560,
...
}]
}active-tab.json is stale, frozen at the welcome page:
{"tabId":1927305561,"url":"http://127.0.0.1:34567/welcome","title":"GStack Browser"}The Side Panel REPL then reports the user is still on the welcome page, even though the visible Chromium window is on a different site.
Expected
tabs.json and active-tab.json should reflect the current real URL / title.
(browse status from CLI works fine — the daemon sees the live URL — so this is isolated to the extension → state-file pipeline.)
Root cause
Permissions, not code. The code path is correct end-to-end:
extension/background.js:521snapshotTabs()callschrome.tabs.query({})and usest.url || ''as a fallback.extension/background.js:571chrome.tabs.onUpdatedlistener firespushTabState('updated').browse/src/terminal-agent.ts:441writestabs.jsonatomically.browse/src/terminal-agent.ts:457writesactive-tab.jsononly ifactive.urlis truthy and notchrome:///chrome-extension://.
The break is at the Chrome boundary: with the current manifest.json,
\"permissions\": [\"sidePanel\", \"storage\", \"activeTab\", \"scripting\"],
\"host_permissions\": [\"http://127.0.0.1:*/\", \"ws://127.0.0.1:*/\"]Chrome MV3 returns tab.url / tab.title only when one of:
\"tabs\"permission is granted (broad), orhost_permissionsmatches the tab's URL, oractiveTabis granted and the user just clicked the extension action on that tab (does not apply to background polling).
For any site outside 127.0.0.1, none of these hold, so Chrome returns undefined for both fields. snapshotTabs()'s || '' fallback then writes empty strings, and the active-tab gate (if (active && active.url && ...)) silently skips the write.
This means the feature has never worked outside localhost since it shipped — not a regression from a later version.
Git timeline
7665adf4(v0.12.0) — extension introduced; only listened to extension's own messaging, didn't read arbitrary tab URLs.dc0bae82(v0.12.6.0) — "sidebar agent uses real tab URL instead of stale Playwright URL" (related but different code path).ed1e4be2(v1.14.0.0) — added thechrome.tabs.onUpdatedlistener +tabs.jsonwriter for live tab awareness, but the manifest diff only added\"ws://127.0.0.1:*/\"for the new PTY channel; tab-related permissions weren't broadened.
Proposed fix
One line in extension/manifest.json. Two reasonable options:
Option A (minimal scope, recommended) — add \"tabs\" permission:
```diff
- "permissions": ["sidePanel", "storage", "activeTab", "scripting"],
- "permissions": ["sidePanel", "storage", "activeTab", "scripting", "tabs"],
```
The \"tabs\" permission is a no-warning install-time grant in MV3 (it's a non-host permission). It exposes tab.url / tab.title / tab.favIconUrl to chrome.tabs.query() etc. across all tabs, but does not grant content-script injection or cross-origin fetch — so it's a tight scope match for what snapshotTabs() actually needs.
Option B — broaden host permissions:
```diff
- "host_permissions": ["http://127.0.0.1:/", "ws://127.0.0.1:/"],
- "host_permissions": ["http://127.0.0.1:/", "ws://127.0.0.1:/", "<all_urls>"],
```
This works but triggers the install-time "Read your browsing history on all websites" warning. Heavier than necessary.
Recommend Option A.
Test idea
Add an e2e test in the headed-mode harness that:
- Navigates the Playwright-controlled Chromium to a non-localhost URL (e.g.,
https://example.com). - Reads
tabs.jsonafter a 1s settle. - Asserts
tabs[0].url === 'https://example.com/'andtabs[0].titleis non-empty.
Without the manifest fix, this test fails with empty strings — guards against the same regression on future MV3 changes.
Environment
- gstack
v1.15.0.0(also reproduces by inspection of v1.14.x manifest) - macOS 25.4.0 (Darwin)
- Chromium launched via
gstack browse connect(Playwright-managed, headed)
Found by reading state files + extension/manifest.json + background.js + terminal-agent.ts after a Side Panel session reported "can't see current tab." Happy to PR if useful.
Activity
time-attack commentedon Jul 15, 2026
@16francej Please close this issue as resolved. The extension now has the tabs permission and a regression test through #1431, so URLs and titles work on normal non-localhost pages.
capy-ai commentedon Oct 3, 2026
The sidebar extension now requests the
tabspermission (extension/manifest.json:7), sotabs.jsonandactive-tab.jsoncarry the real URL and title on non-localhost pages; checked on v1.91.16.0.Fixed in v1.32.0.0 (#1431).
If you still see this on the latest gstack, please reopen with details.