[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

Status Open
Reported on v2.1.220
Maintainer reply None cached
Activity 6 comments · opened Aug 13, 2026

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

  1. Add a hosts entry: 127.0.0.1 local-subdomain.real-domain.com (any public-looking domain).
  2. Serve a local SPA on that hostname over HTTPS (mkcert cert; Angular dev server, port 4200).
  3. In Claude Code, open the Browser pane at https://local-subdomain.real-domain.com:4200.
  4. 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 *.localhost subdomains (same class: local-dev origins over-blocked)
  • #75289 — open feature request: allow whitelisting localhost/local addresses

View original on GitHub ↗

5 Comments

nrightmireUHL · 17 days ago

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 with net::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

  • OS: Windows 11 Pro 10.0.26100
  • Claude Code desktop app, version: Claude 1.28929.0 (d1a6bc) 2026-08-11T18:58:06.000Z
  • Update installed: on or about 2026-08-11 (I saw the "restart to update" prompt; exact version before/after not recorded)
  • First observed broken: 2026-08-12
  • Local server: IIS, HTTPS on port 443
  • Hostname: mapped to 127.0.0.1 via the Windows hosts file, e.g. dev.example.testnot localhost:PORT

Reproduction

  1. Add a hosts-file entry mapping dev.example.test to 127.0.0.1.
  2. Serve a site from a local web server over HTTPS on port 443 at that hostname. The page should load its own same-origin CSS and JS (any Bootstrap + jQuery page will do).
  3. In the Claude Code desktop app, open the Browser pane at 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:

Failed to load resource: net::ERR_BLOCKED_BY_CLIENT     (x9)
Uncaught ReferenceError: $ is not defined

Computed styles confirm no CSS applied:

getComputedStyle(document.body).fontFamily   // "Times New Roman"  (UA fallback)
getComputedStyle(button).backgroundColor     // "rgb(240, 240, 240)"  (UA default)
getComputedStyle(button).borderRadius        // "0px"
typeof window.jQuery                         // "undefined"
document.images[0].complete                  // true
document.images[0].naturalWidth              // 0

A plain fetch() from the page's own context, for its own same-origin assets, fails:

await fetch('/css/general.css')                       // TypeError: Failed to fetch
await fetch('/lib/jquery/3.7.1/jquery-3.7.1.min.js')  // TypeError: Failed to fetch
await fetch('/images/logo.png')                       // TypeError: Failed to fetch

Reading rules from a same-origin stylesheet throws, which suggests the response is being replaced with an opaque one rather than simply failing:

document.styleSheets[0].cssRules   // SecurityError

Key asymmetry

On the very same blocked page, a cross-origin script from a public CDN loads and executes fine:

typeof window.turnstile   // "object"   (https://challenges.cloudflare.com/turnstile/v0/api.js)

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

  1. preview_start with {url: "https://dev.example.test"}. Returns "type": "browser". Still blocked.
  2. Forced reload after that origin approval. Still blocked.
  3. .claude/launch.json entry to register it as a local server:

``json
{
"version": "0.0.1",
"configurations": [
{ "name": "local-dev", "url": "https://dev.example.test", "port": 443 }
]
}
`
preview_start with {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.

  1. Plain 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 *.localhost subdomain, 127.0.0.1, and ::1. A hosts-file hostname that resolves to 127.0.0.1 is not in that list, and preview_start classifying 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:PORT cannot route to the right site.

Side issue

computer{action: "screenshot"} fails whenever the pane is not visible on screen:

screenshot failed: Screenshot timed out after 5s: the Browser pane is not
displayed, so the page is not compositing frames. Display the pane and retry.

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') reports responseStatus: 0 and transferSize: 0 for 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.

cheekyboots · 16 days ago

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:

  • I'm running ServBay on MacOS.
  • I'm on Claude for Mac 1.30096.1 (194d93) 2026-08-13T21:28:02.000Z
mklpsn · 15 days ago

Confirming this on another setup (macOS, Claude Code Browser pane). Local hostname app.example.com127.0.0.1 via /etc/hosts, served HTTPS by nginx. Same symptom: document loads 200, every subresource fails net::ERR_BLOCKED_BY_CLIENT, page renders unstyled. Worked for weeks, then started blocking with no local changes.

A few data points to add:

  • Hostname-keyed guard. Same app, same session: https://app.example.com → all subresources blocked; http://127.0.0.1:8000 (literal loopback) → all 200; public-IP sites → all 200. Only the registered-hostname-to-loopback case is blocked.
  • Not auth-related. It coincided with a "Couldn't authenticate Claude Code" error, but the block persisted unchanged after auth was fixed — two separate regressions.
  • Not in Claude-in-Chrome. The identical URL loads fully there, so it's specific to the desktop Browser pane.

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.1 also works but loses HTTPS and the real host. A user-approvable trusted-origin allowlist (#86228) would be the real fix.

StephenCoreSchedule · 15 days ago

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/hosts subdomains 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() fails net::ERR_BLOCKED_BY_CLIENT (even /robots.txt and /favicon.ico); public sites are fine; a literal http://localhost:<port> origin is fully fine (all assets 200).

New data points:

  1. The quarantine is bidirectional. From a page ON the flagged domain, 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, a localhost-origin page cannot fetch from the flagged domain.
  2. A .claude/launch.json attach 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.
  3. Multi-tenant apps have no localhost workaround. For vhost/cname-based apps the hostname is the tenant discriminator; 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/hosts would both solve this — hosts-file entries are user-configured, not attacker-controlled DNS.

dlindberg · 15 days ago

Also affects DDEV — and the hostname is not in /etc/hosts, which matters for the proposed fixes

Reproduces 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 subresource net::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.site is a real public wildcard A record pointing at 127.0.0.1, maintained by the DDEV project — every DDEV project gets one by default, with no /etc/hosts entry involved:

$ dig +short my-project.ddev.site @1.1.1.1
127.0.0.1

$ grep my-project /etc/hosts
(no match)

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 :443 and the Vite dev server on :5173:

performance.getEntriesByType('resource')
  .map(r => [r.name, r.transferSize, r.responseStatus])
// every entry: 0, 0 — 14 on https://<project>.ddev.site, 3 on https://<project>.ddev.site: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.

https://<project>.ddev.site/js/filament/filament/app.js   as navigation  → 11,496 bytes
                                    same URL, same page   as subresource →      0 bytes

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 localhost workarounds are awkward here specifically: DDEV routes by Host header through a shared router container, so several projects sit behind the same :80/:443localhost:PORT can'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 --mcp renders the same page fully.

Showing cached comments. Read the full discussion on GitHub ↗