[BUG] Browser pane blocks same-origin subresources with ERR_BLOCKED_BY_CLIENT for local dev domains mapped to 127.0.0.1 via /etc/hosts — page renders blank
Title: [BUG] Browser pane blocks same-origin subresources with ERR_BLOCKED_BY_CLIENT for local dev domains mapped to 127.0.0.1 via /etc/hosts — page renders blank
Environment
- Claude Code: 2.1.220 (desktop app, macOS)
- OS: macOS (Darwin 25.5.0), Apple Silicon
- Surface: Browser pane (
mcp__Claude_Browser__*/ preview pane), NOT the Claude in Chrome extension
Summary
Since 2026-08-13, the Browser pane blocks all subresource requests (scripts, stylesheets, XHR/fetch) to hostnames that look public but resolve to 127.0.0.1 via /etc/hosts, failing them with net::ERR_BLOCKED_BY_CLIENT. Top-level navigations to the same origin succeed, so the page's HTML loads but renders blank (no JS/CSS executes).
This breaks a very common local dev setup: a custom hostname (e.g. myname-admin.example.com) mapped to loopback in /etc/hosts, served over HTTPS with an mkcert certificate — used so local apps share the production cookie domain / TLS setup. It worked in the Browser pane until 2026-08-13 with no local changes (same project, same server, same certs), which suggests a desktop-app update or a server-side URL-classification change.
Steps to reproduce
- Add a hosts entry:
127.0.0.1 local-subdomain.real-domain.com(any public-looking domain). - Serve a local SPA on that hostname over HTTPS (mkcert cert; Angular dev server, port 4200).
- In Claude Code, open the Browser pane at
https://local-subdomain.real-domain.com:4200. - Page loads (title appears, document request → 200) but renders blank.
Evidence
Network log from the pane — the document loads, every subresource is blocked client-side:
GET https://local-subdomain.real-domain.com:4200/ → 200 OK
GET https://local-subdomain.real-domain.com:4200/styles.css → FAILED: net::ERR_BLOCKED_BY_CLIENT
GET https://local-subdomain.real-domain.com:4200/runtime.js → FAILED: net::ERR_BLOCKED_BY_CLIENT
GET https://local-subdomain.real-domain.com:4200/polyfills.js→ FAILED: net::ERR_BLOCKED_BY_CLIENT
GET https://local-subdomain.real-domain.com:4200/styles.js → FAILED: net::ERR_BLOCKED_BY_CLIENT
GET https://local-subdomain.real-domain.com:4200/scripts.js → FAILED: net::ERR_BLOCKED_BY_CLIENT
GET https://local-subdomain.real-domain.com:4200/main.js → FAILED: net::ERR_BLOCKED_BY_CLIENT
Isolation tests performed from the same pane session:
| Test | Result |
|---|---|
| curl -I to the dev server from the shell | ✅ 200 OK (server healthy) |
| Top-level navigation to the origin | ✅ allowed |
| Top-level navigation directly to /main.js | ✅ allowed — file loads fine |
| Same-origin subresource/fetch('/main.js') from the page | ❌ ERR_BLOCKED_BY_CLIENT / Failed to fetch |
| fetch() to a second loopback-mapped domain (api-local-subdomain.real-domain.com, local API) | ❌ blocked |
| Subresources of a genuinely public site (angular.dev) in the same pane | ✅ all load |
| https://localhost:4200 fallback | ❌ ERR_CERT_COMMON_NAME_INVALID (mkcert SAN only covers the custom domain — expected) |
So the blocking is target-based: requests to "public-looking hostname → loopback IP" are blocked when issued as subresources, while user-approved navigations pass. It looks like an anti-DNS-rebinding protection that doesn't account for the user's own /etc/hosts mappings of their approved origin.
Expected behavior
Once the user approves navigation to an origin (permission card), same-origin subresources — and ideally requests to other explicitly hosts-file-mapped local domains — should load. At minimum there should be a way to allowlist these domains (as requested in #75289).
Impact
- Any local dev workflow using hosts-file custom domains + HTTPS (mkcert) is unusable in the Browser pane: pages render blank and API calls to local backends are blocked.
- Claude's own runtime UI verification loops (screenshot/read_page-driven) silently break; the failure is only diagnosable via the network log.
- Workaround we're using: drive the real Chrome via the Claude in Chrome extension instead of the pane.
Related issues
- #43279 — Chrome extension blocks a legitimate dev subdomain while allowing the production domain
- #47195 — Preview blocks
*.localhostsubdomains (same class: local-dev origins over-blocked) - #75289 — open feature request: allow whitelisting localhost/local addresses
Showing cached comments. Read the full discussion on GitHub ↗
5 Comments
I am experiencing the same issues:
Summary
In the Claude Code desktop app, the built-in Browser pane (
mcp__Claude_Browser__*) loads the top-level HTML document for a local site served over HTTPS on a hosts-file hostname, but blocks every subresource that origin requests withnet::ERR_BLOCKED_BY_CLIENT. CSS, JS, and images all fail, so pages render as unstyled text and bare form fields.The same URL renders correctly in Chrome. Public sites render correctly in the pane. A cross-origin public CDN script on the same blocked page loads and executes normally, so this is not a blanket subresource block — only the page's own loopback origin is affected.
This previously worked. It broke following an app update, so I believe it is a regression rather than intended behavior.
Environment
Claude 1.28929.0 (d1a6bc) 2026-08-11T18:58:06.000Z127.0.0.1via the Windows hosts file, e.g.dev.example.test— notlocalhost:PORTReproduction
dev.example.testto127.0.0.1.https://dev.example.test.Expected
The page renders as it does in a normal browser, with its stylesheets, scripts, and images loaded.
Actual
The HTML document loads correctly — right title, complete DOM, all text and form fields present — but every subresource is blocked.
Evidence
Console:
Computed styles confirm no CSS applied:
A plain
fetch()from the page's own context, for its own same-origin assets, fails:Reading rules from a same-origin stylesheet throws, which suggests the response is being replaced with an opaque one rather than simply failing:
Key asymmetry
On the very same blocked page, a cross-origin script from a public CDN loads and executes fine:
And in the same pane session, a public site renders perfectly, including its own same-origin CSS and web fonts (verified with
https://getbootstrap.com/docs/5.3/examples/album/).So: public origin's own subresources → allowed. Loopback origin's own subresources → blocked. Public cross-origin subresource on the blocked page → allowed.
Control
The identical URL renders correctly in Chrome, with full styling. The site itself is healthy.
Workarounds attempted — none worked
preview_startwith{url: "https://dev.example.test"}. Returns"type": "browser". Still blocked..claude/launch.jsonentry to register it as a local server:``
json
`{
"version": "0.0.1",
"configurations": [
{ "name": "local-dev", "url": "https://dev.example.test", "port": 443 }
]
}
preview_startwith{name: "local-dev"}attaches cleanly — response says "Attached the preview to the configured url; no process was started" and no longer reports"type": "browser"` — but subresources are still blocked, with identical symptoms.http://. Inconclusive: the server 301-redirects to HTTPS, so the test never ran over HTTP.Possibly relevant
The docs state that localhost addresses open directly, listing
localhost, any*.localhostsubdomain,127.0.0.1, and::1. A hosts-file hostname that resolves to127.0.0.1is not in that list, andpreview_startclassifying it as"type": "browser"is consistent with it being treated as an external site. If the local-server allowlist matches on the literal hostname rather than the resolved address, that would explain which requests get blocked — though not why it changed, since this configuration worked before the update.Custom hostnames on loopback are a common setup for local development against host-header-based virtual hosts, where
localhost:PORTcannot route to the right site.Side issue
computer{action: "screenshot"}fails whenever the pane is not visible on screen:This compounds the above — it pushes you toward
read_page, whose accessibility-tree output looks "stripped" for an entirely unrelated reason, which makes the real bug harder to identify.Separately,
performance.getEntriesByType('resource')reportsresponseStatus: 0andtransferSize: 0for all entries, including ones that demonstrably succeeded, so resource timing cannot be used to diagnose which requests actually failed.Impact
Local development against hosts-file hostnames cannot be visually verified in the pane at all. Any layout, icon, or JS-driven UI check silently produces a page that looks broken, which reads as a defect in the code under test rather than a browser limitation.
Same issue starting today. It will load http://127.0.0.1 fine but not custom domains set up for development that use /etc/hosts. Loads the HTML and then blocks every subresource with ERR_BLOCKED_BY_CLIENT
Env:
Confirming this on another setup (macOS, Claude Code Browser pane). Local hostname
app.example.com→127.0.0.1via/etc/hosts, served HTTPS by nginx. Same symptom: document loads200, every subresource failsnet::ERR_BLOCKED_BY_CLIENT, page renders unstyled. Worked for weeks, then started blocking with no local changes.A few data points to add:
https://app.example.com→ all subresources blocked;http://127.0.0.1:8000(literal loopback) → all200; public-IP sites → all200. Only the registered-hostname-to-loopback case is blocked.Workaround that works: a public tunnel. Exposing the local server through a tunnel (e.g. cloudflared) gives it a public-IP hostname, which the guard allows — all static loads and HTTPS/host-bound behavior is preserved. Using literal
127.0.0.1also works but loses HTTPS and the real host. A user-approvable trusted-origin allowlist (#86228) would be the real fix.Confirming on another setup, with three data points I haven't seen in the thread yet.
Env: Claude Code CLI 2.1.177, macOS (Darwin 27.0.0), Apple Silicon. Local multi-tenant dev: ~150
/etc/hostssubdomains of a real domain → 127.0.0.1, Caddy terminating TLS (local CA), app containers behind it. Worked until recently with no local changes — the first load after the change still served assets from disk cache from a prior working session, so the flip is recent.Same core symptom as the OP: top-level document loads, every same-origin subresource and
fetch()failsnet::ERR_BLOCKED_BY_CLIENT(even/robots.txtand/favicon.ico); public sites are fine; a literalhttp://localhost:<port>origin is fully fine (all assets 200).New data points:
fetch('http://localhost:8081/…')(literal loopback, healthy server) is also blocked — the flagged page is cut off from the entire local network in both directions, while its access to the public internet still works (google.com / jsdelivr fetches succeed from the same page). Conversely, alocalhost-origin page cannot fetch from the flagged domain..claude/launch.jsonattach entry does not exempt the domain. Registering{"name": "…", "url": "https://<hosts-mapped-domain>"}and opening it via the preview flow attaches and navigates fine, but subresources are still blocked — the dev-server registration path goes through the same policy.localhost:<port>reaches the app but can't express any tenant. For these, the pane is currently unusable.A per-origin approval (like the existing first-load card) or trusting hostnames the local resolver maps to loopback via
/etc/hostswould both solve this — hosts-file entries are user-configured, not attacker-controlled DNS.Also affects DDEV — and the hostname is not in
/etc/hosts, which matters for the proposed fixesReproduces on macOS 26.5.2, Claude Code 2.1.223 (so still present well past the 2.1.177 report above), DDEV v1.25.3, Laravel app behind DDEV's nginx/router over HTTPS.
Same core symptom: document
200, every subresourcenet::ERR_BLOCKED_BY_CLIENT, page renders as unstyled HTML.The data point I want to add: this hostname resolves through public DNS, not the hosts file.
*.ddev.siteis a real public wildcard A record pointing at127.0.0.1, maintained by the DDEV project — every DDEV project gets one by default, with no/etc/hostsentry involved:That resolves for any name you care to invent — it's a wildcard, so the command above reproduces as written without a DDEV install.
So the guard is keyed on the resolved address, not on hosts-file membership. That's worth flagging because two of the fixes proposed in this thread and in #86228 are scoped to hosts-file entries ("trusting hostnames the local resolver maps to loopback via
/etc/hosts" — hosts entries being user-configured rather than attacker-controlled). A fix written to that scope would ship while every DDEV project stayed broken. Whatever the eventual rule is, it needs to cover any hostname that resolves to loopback, however it resolves.Measurements, consistent with the OP:
All 17 subresources on the page report
transferSize: 0,responseStatus: 0— spanning two origins, the app on:443and the Vite dev server on:5173:Control, which I think is the sharpest form of the "top-level is fine, subresource is not" asymmetry: navigating the pane directly to one of the blocked asset URLs returns the file in full.
Identical host, port, certificate and socket. Approving that exact URL through the per-origin navigation card does not license it as a subresource — consistent with the report above that the launch.json attach path doesn't exempt it either.
The console shows the bare client-block code with no private-network diagnostic alongside it, which points at an embedder-level request filter rather than Chromium's private-network-access rule. I can establish the behaviour but not the mechanism.
Why the
localhostworkarounds are awkward here specifically: DDEV routes byHostheader through a shared router container, so several projects sit behind the same:80/:443—localhost:PORTcan't select a project, the same shape as the multi-tenant vhost case above. And because the Vite dev server derives its origin from the project URL, assets arrive from a second port on that hostname, so an origin-laundering proxy has to launder two origins rather than one.For anyone else on DDEV who lands here: a non-Chromium driver is unaffected — driving Safari Technology Preview over
safaridriver --mcprenders the same page fully.