[FEATURE] Desktop browser preview: auto-allow RFC 6761 reserved TLDs (*.test) that resolve to loopback, like *.localhost
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:
- Approve navigation to
https://myapp.test("Always allow"). - Approve it again for
http://myapp.testif the redirect or a link uses plain http (scheme counts as a separate origin). - 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:
- Auto-allow reserved local TLDs pinned to loopback. On first visit, if the host ends in
.test/.localhostand resolves to a loopback address (127.0.0.0/8,::1), allow it silently and record the resolved address inlaunchPreviewPrivateNetworkReadPins, 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.testname being re-pointed later. - Wildcard / pattern support in
launchPreviewAllowedOriginsandlaunchPreviewPrivateNetworkReadOrigins, e.g."https://*.test","*://*.test". - 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.jsonwhile the app is closed, adding the origin to both lists plus a127.0.0.1pin. 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.jsonwith"url": "https://myapp.test": still hits both prompts;launch.jsononly auto-trustslocalhostorigins.- Using Claude in Chrome instead of the built-in pane: loses
preview_start/preview_logsintegration and the whole point of the built-in Browser pane.
Priority
High - Significant impact on productivity
Feature Category
Configuration and settings
Use Case Example
herd linkinD:\Herd\myapp→ site is served onhttps://myapp.test, resolving to127.0.0.1.- Ask Claude Code Desktop to fix a layout bug and verify it in the preview.
- Claude runs
preview_startwith the.testURL → prompt 1 (navigate). Approve. - Claude runs
read_page/ screenshot → prompt 2 (private-network read). Approve. - Page redirects to
http://once → prompt 3. - 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*.testscenario, 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
.testfor testing; §6.3 reserves.localhost. Browsers already special-case.localhostas loopback (Chrome resolves*.localhostto loopback without DNS)..testis 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.
Showing cached comments. Read the full discussion on GitHub ↗
4 Comments
up
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.
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.testis already inlaunchPreviewAllowedOriginsandlaunchPreviewPrivateNetworkReadOrigins(pinned to127.0.0.1) inclaude_desktop_config.json.bypassPermissionsmode.Reserved TLDs like
.test(RFC 6761) resolving to loopback should be treated likelocalhost, or at least offer a persistent Always allow for them.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.