[Bug] MCP `navigate` to localhost auto-denied by 30s popup; `bypassLocalhostForMcp` flag never enabled
Bug Description
# Claude-in-Chrome — MCP navigate on localhost is gated by a 30s auto-deny popup; the bypassLocalhostForMcp flag exists but is never enabled
## Summary
When driving a local dev server over MCP (Claude Code → extension), navigate to a
localhost origin frequently returns Permission denied by user and only succeeds
after a manual retry. The extension already contains a bypassLocalhostForMcp code
path that would fix this, but it is never enabled in the shipping build, there is no
setting to turn it on, and grants are not persisted — so the friction repeats on
every fresh tab.
## Environment
- Extension: "Claude" fcoeoabgfenejglbffodgkkbkcdhcgfn, v1.0.84 (latest on the
Web Store as of 2026-08 — update endpoint returns 1.0.84).
- Chrome 151, macOS.
- Client: Claude Code CLI over the MCP bridge / native messaging.
- Chrome host access: <all_urls> granted, active, withholding_permissions:false
("On all sites"). Claude Code allowlist: mcp__claude-in-chrome__navigate allowed.
So neither Chrome host permissions nor the client allowlist is the gate.
## Root cause (from the shipped bundle, assets/mcpPermissions-*.js + service-worker.ts-*.js)
1. PermissionManager.checkPermission(url) exempts passive reads (screenshot /
read_page / find) but gates navigate (and click / execute_javascript) to a new
origin as a DOMAIN_TRANSITION → permission_required. Observed effect:
screenshot succeeds while navigate is denied on the same tab.
2. A bypassLocalhostForMcp flag exists:
``js
if (!forcePrompt && bypassLocalhostForMcp && isLocalhostUrl(url))
return { allowed: true, needsPrompt: false };
`
but every PermissionManager is constructed as new kD(() => false, { trackEvent })
— the option is never passed, so it defaults to false. Grep of the whole bundle
shows bypassLocalhostForMcp is **never set to true anywhere**. It is effectively
dead code in 1.0.84. There is also no user setting to enable it.
3. The approval handler opens a 600×600 focused:true popup and **auto-denies after
30s**, and denies immediately if the window can't be created/focused:
`js
chrome.windows.create({ url: "sidepanel.html?...&mcpPermissionOnly=true", type:"popup",
width:600, height:600, focused:true }, w => { w ? ... : a(false) });
setTimeout(() => a(false), 30000);
`
When an agent navigates a freshly-created/background tab, the focused popup races
with the agent's next step and the 30s deadline elapses → Permission denied by user
(errorCode: permission_denied_user).
4. On approval the grant is lA.ONCE (netloc scope), **not** ALWAYS:
`js
grantPermission({ type:"netloc", netloc: url.host }, lA.ONCE, toolUseId, url.origin)
`
so nothing is persisted — every fresh tab / new turn re-prompts.
## Reproduction
1. Run any local dev server (e.g. http://localhost:5937).
2. From Claude Code, tabs_create_mcp (new blank tab) then navigate to the localhost URL.
3. Observe intermittent Permission denied by user; a manual retry (with the popup
approved in time) succeeds. Repeat on another fresh tab → prompts again.
## Impact
Automated browser testing against local dev servers is unreliable: navigations to
localhost stall on a focus-racing popup with a hard 30s deadline, and approvals don't
stick. This is the single most common failure mode in local /tf-style testing.
## Suggested fixes (any one helps; combined is ideal)
1. **Enable bypassLocalhostForMcp by default** (or expose a setting), since
127.0.0.1 / localhost / *.localhost are inherently local and low-risk.
2. **Persist an "always" grant per origin** from the approval popup (offer
Once / Always), so repeated navigations to an approved dev origin don't re-prompt.
3. **Don't hard auto-deny on the 30s timeout for a still-open popup** — leave it
pending, or surface a distinct timeout/no_response errorCode instead of
permission_denied_user, which misleadingly reads as an explicit user denial.
4. Avoid denying when the focused popup can't grab focus; retry/reshow instead.
## Workarounds we use today (v1.0.84)
- Reuse an already-loaded localhost tab and drive via in-page clicks (no
DOMAIN_TRANSITION) instead of tabs_create_mcp + navigate`.
- Approve the popup within 30s, and expect to repeat it (grants are ONCE).
Environment Info
- Platform: darwin
- Terminal: iTerm.app
- Version: 2.1.220
- Feedback ID: fce6ac9d-662a-4bd7-af3c-ddf197f4bd13
Errors
[]