[FEATURE] Desktop browser preview: auto-allow RFC 6761 reserved TLDs (*.test) that resolve to loopback, like *.localhost

Status Open
Reported on v2.1.250
Maintainer reply None cached
Activity 5 comments · opened Sep 7, 2026

Preflight Checklist

  • [x] I have searched existing requests and this feature hasn't been requested yet
  • [x] This is a single feature request (not multiple features)

Problem Statement

The Claude Desktop app (Code tab) built-in Browser pane treats only localhost, *.localhost, 127.0.0.1 and ::1 as local dev servers. Every other origin goes through the "allow this site" flow, and on top of that through a second, separate gate for private-network read tools (read_page, get_page_text, screenshots).

Laravel Herd, Valet and Laragon serve every site on *.test by default. That is the standard local PHP/Laravel/WordPress setup on macOS and Windows. .test is a TLD reserved by RFC 6761, exactly like .localhost, and can never be a routable public domain.

In practice this means that for each project I have to:

  1. Approve navigation to https://myapp.test ("Always allow").
  2. Approve it again for http://myapp.test if the redirect or a link uses plain http (scheme counts as a separate origin).
  3. Approve the private-network read gate separately, otherwise read_page / screenshot fail with "This site requires per-action approval; Browser read tools are not available on it".

Each approval lands as a separate literal entry in claude_desktop_config.json. After a few projects my preferences block looks like this (real, trimmed):

"launchPreviewAllowedOrigins": [
  "https://app-one.test",
  "http://app-two.test",
  "https://app-three.test",
  "https://app-four.test",
  "http://app-four.test",
  "https://app-five.test"
],
"launchPreviewPrivateNetworkReadOrigins": [
  "https://app-four.test",
  "https://app-one.test"
],
"launchPreviewPrivateNetworkReadPins": {
  "https://app-four.test": ["127.0.0.1"],
  "https://app-one.test": ["127.0.0.1"]
}

Note the Pins block: the app already resolves these origins and pins them to 127.0.0.1. So the loopback check exists, it just is not used to short-circuit the permission flow.

The alternative, switching Herd to *.localhost, is a global setting that affects every site on the machine and breaks tooling that assumes the Herd default (herd link, herd secure, shared .env files across a team).

Proposed Solution

Any one of these would solve it; the first is the cleanest:

  1. Auto-allow reserved local TLDs pinned to loopback. On first visit, if the host ends in .test / .localhost and resolves to a loopback address (127.0.0.0/8, ::1), allow it silently and record the resolved address in launchPreviewPrivateNetworkReadPins, exactly as the manual "Always allow" flow does today. On every later visit require both conditions: the host still resolves to loopback and the result matches the stored pin. Any drift (different address, non-loopback, NXDOMAIN) drops the automatic trust and falls back to the current prompt. The pin mechanism already exists; this only changes who creates the first pin (the app instead of a click). Trusting the live lookup alone would remove the one check that catches a .test name being re-pointed later.
  2. Wildcard / pattern support in launchPreviewAllowedOrigins and launchPreviewPrivateNetworkReadOrigins, e.g. "https://*.test", "*://*.test".
  3. A trusted-local-TLD preference, e.g. "launchPreviewTrustedLocalTlds": ["test"], applied with the same loopback check as in (1).

Also, regardless of the above: an "Always allow" approval for one origin should cover both the navigation gate and the private-network read gate, and http/https for the same host. Today they are three independent prompts, which is what makes this feel broken rather than merely strict.

Alternative Solutions

  • Manually editing claude_desktop_config.json while the app is closed, adding the origin to both lists plus a 127.0.0.1 pin. Works, but is undocumented, fragile, and has to be repeated per project and per scheme.
  • Herd on *.localhost: global change, breaks team conventions and Herd defaults, see above.
  • .claude/launch.json with "url": "https://myapp.test": still hits both prompts; launch.json only auto-trusts localhost origins.
  • Using Claude in Chrome instead of the built-in pane: loses preview_start / preview_logs integration and the whole point of the built-in Browser pane.

Priority

High - Significant impact on productivity

Feature Category

Configuration and settings

Use Case Example

  1. herd link in D:\Herd\myapp → site is served on https://myapp.test, resolving to 127.0.0.1.
  2. Ask Claude Code Desktop to fix a layout bug and verify it in the preview.
  3. Claude runs preview_start with the .test URL → prompt 1 (navigate). Approve.
  4. Claude runs read_page / screenshot → prompt 2 (private-network read). Approve.
  5. Page redirects to http:// once → prompt 3.
  6. Next project, repeat from step 3.

With the proposal: step 3 onward is silent, same as it is today for http://localhost:3000.

Additional Context

  • Related: #91495 (open, area:desktop / area:permissions, same Herd *.test scenario, per-call prompts), #78315 (closed as invalid, describes the navigate-vs-read two-gate inconsistency), #27263 (closed as completed, external URL whitelist).
  • Docs: https://code.claude.com/docs/en/desktop.md, section "Open the preview at a specific URL", lists the four auto-allowed local patterns.
  • RFC 6761 §6.2 reserves .test for testing; §6.3 reserves .localhost. Browsers already special-case .localhost as loopback (Chrome resolves *.localhost to loopback without DNS). .test is the de-facto equivalent for local PHP tooling.
  • Environment: Windows 11 Pro 10.0.26220, Claude Desktop (Code tab, built-in browser), Claude Code CLI 2.1.250, Laravel Herd (Windows) serving *.test.

View original on GitHub ↗

4 Comments

maciej-bielski-foreto · 1 month ago

up

m13v · 1 month ago

fix #1 has an edge your own config hints at: auto-allowing whenever DNS returns loopback makes the gate depend on a live lookup nothing re-checks, but your launchPreviewPrivateNetworkReadPins block exists precisely because resolution can drift. a .test host resolving 127.0.0.1 today gets silently trusted, and if it re-points later there's no prompt left to catch it, you removed the one that would have. gating on 'resolves loopback AND matches the stored pin' keeps the check firing where resolution-alone goes quiet.

ren0v · 8 days ago

Same here — Claude Code Desktop on Windows 11, Laravel Herd site https://my-local-site.test (resolves to 127.0.0.1).

  • https://my-local-site.test is already in launchPreviewAllowedOrigins and launchPreviewPrivateNetworkReadOrigins (pinned to 127.0.0.1) in claude_desktop_config.json.
  • Every interaction still raises the card "Site-level permissions are disabled for this site. You'll be asked for each action." with only Allow once — no Always allow option, even with bypassPermissions mode.
  • In practice that's ~50 prompts per session during UI verification, which makes the browser pane unusable for local dev.

Reserved TLDs like .test (RFC 6761) resolving to loopback should be treated like localhost, or at least offer a persistent Always allow for them.

m13v · 8 days ago

the 'at least offer Always allow' fallback wouldn't fix your case: you already have the origin in both lists with a 127.0.0.1 pin, and the per-action card still fires. a persistent button would just write one more entry into config that this path ignores. the site-level disabled state overrides the stored lists, so that override needs to respect them first.

Showing cached comments. Read the full discussion on GitHub ↗