Skip to Content
webagent docs
Guides

Test a local app

Why localhost does not work from a cloud browser, and how to test an app on your machine or network with a tunnel or your own proxy.

Open as .md

The browser that webagent runs lives in the cloud. It cannot open http://localhost:3000 on your laptop, because that address means "this machine", and "this machine" is our server.

This page explains what happens if you try, and the two ways that work:

  1. A tunnel to your dev server. Best for one developer on a laptop.
  2. Your own proxy plus an allow list. For an app on a company network.

If your app is already on a public preview or staging URL, skip all of this and use that URL. See Protected sites if it is behind a gate.

Why localhost fails

Every navigation passes an SSRF guard. The guard refuses localhost, *.localhost, *.internal, loopback addresses (127.0.0.1), link-local and cloud metadata addresses, and private addresses such as 192.168.x.x or 10.x.x.x. No option you can send turns it off. It protects our network and yours.

If you pass such a URL as startUrl, session creation fails with HTTP 403:

create_session with startUrl http://localhost:3000
{
  "error": {
    "code": "navigation_blocked",
    "message": "startUrl http://localhost:3000 was blocked (private, link-local and metadata addresses are not reachable): navigation blocked: internal host",
    "details": { "url": "http://localhost:3000", "reason": "internal host", "fields": ["startUrl"] }
  }
}

The reason changes with the address:

Addressreason
localhost, *.localhost, *.internalinternal host
127.0.0.1 and other loopback or link-local addressesblocked address 127.0.0.1
192.168.1.20, 10.0.0.5private address 192.168.1.20

If the agent calls navigate inside a running session, the same refusal comes back as navigation blocked: internal host. A click or redirect onto a refused host comes back as navigation to <host> was blocked: <reason>. A blocked test is a test that could not run. It is not a bug in your app.

Option 1: a tunnel

A tunnel gives your local port a public HTTPS address. The cloud browser opens that address, and the tunnel forwards the traffic to your machine. The tunnel host resolves to a public IP, so the guard lets it through.

A tunnel URL is public. Anyone who has the URL can open your dev server, and the agent can click anything on it. Use test data. Do not tunnel an app that holds real customer data. Stop the tunnel when the test is done.

Start your app

Run your dev server as usual and note the port, for example 3000.

Start a tunnel in a second terminal

Pick one of the two tools.

Install cloudflared (see the Cloudflare quick tunnel page), then run:

cloudflared tunnel --url http://localhost:3000

It prints a temporary https://<random-words>.trycloudflare.com URL. You need no Cloudflare account. The URL changes every time and stops working when you stop cloudflared. A quick tunnel does not support server-sent events (SSE) and handles up to 200 requests at once.

Check the URL yourself

Open the tunnel URL in your own browser. You should see your app. If your dev server answers with a "host not allowed" error, add the tunnel host to its allow list. In Vite that is server.allowedHosts.

Give the URL to the agent

In Claude Code or Codex, ask for the test with the tunnel URL, not localhost:

Smoke test https://<random-words>.trycloudflare.com. Log in with the test user and
check that the dashboard shows "Welcome". Give me the verdict and the failed steps.

Or call smoke_test yourself with url set to the tunnel URL, or run_task with startUrl. See MCP tools.

Stop the tunnel

Press Ctrl+C in the tunnel terminal. The URL stops working at once.

We ran a webagent session through an ngrok free tunnel to a static page. Without the ngrok-skip-browser-warning header the browser saw ngrok's warning page ("You are about to visit: ..."). With the header, in the session headers option, it saw the page. We have not run the cloudflared steps.

ngrok warning page

The ngrok free plan shows an interstitial page in front of all HTML browser traffic. The agent would see that page instead of your app. ngrok skips it for requests that carry a header named ngrok-skip-browser-warning (any value). ngrok's free plan limits page describes it.

Send that header with the session headers option. It goes only to the host you name. run_task and smoke_test take it directly, so one call is enough. The steps are in Protected sites.

Option 2: your own proxy and an allow list

If the app lives on a private network, for example a staging server at 10.0.0.5, there is one supported route. You run a proxy that can reach the app. The session sends its traffic through your proxy, and an allow list tells the guard which private hosts to accept.

Body of POST /v1/sessions
{
  "proxyUrl": "http://user:[email protected]:3128",
  "allowHosts": ["10.0.0.5"],
  "startUrl": "http://10.0.0.5:8080"
}

How it works:

  • allowHosts takes hosts, IPs or CIDRs. It is only valid together with proxyUrl, or with a saved proxy (proxyId). Without one of them the request fails with invalid_input.
  • The proxy URL is http://user:pass@host:port or socks5://host:port.
  • Private hosts are reached through your proxy, never from our network. Our own network stays blocked even if you list it.
  • Loopback, link-local and metadata addresses stay blocked, even in allowHosts. This option cannot open localhost.
  • Our servers must be able to reach your proxy over the internet. A proxy behind your laptop's NAT does not work. A proxy on a company network with a public address and a password does.

run_task takes proxyUrl and allowHosts itself, so one call is enough. smoke_test and a skill run take them inside their session object:

smoke_test arguments
{
  "url": "http://10.0.0.5:8080",
  "expectText": "Welcome",
  "session": { "proxyUrl": "http://user:[email protected]:3128", "allowHosts": ["10.0.0.5"] }
}

run_task refuses these options together with sessionId: put them on create_session instead. See Sessions and Skills.

We have not run this route against a real corporate proxy for this page.

Which one to pick

Your app isUse
On a public preview or staging URLThat URL. No tunnel.
On your laptopA tunnel.
On a private network, and you can run a public proxyproxyUrl and allowHosts.
Public but behind a WAF, Vercel protection or Cloudflare AccessProtected sites.

On this page