zeroclaw-labs/zeroclaw · error

Blocked local/private host: {host}

Error message

Blocked local/private host: {host}

What it means

browser_open classifies the URL's host via domain_guard::is_private_or_local_host (loopback, private LAN ranges, link-local, localhost-style names). Private hosts are blocked by default as an SSRF guard; the only escape hatch is listing the host in allowed_private_hosts, in which case validation accepts the URL early.

Source

Thrown at crates/zeroclaw-tools/src/browser_open.rs:70

        }

        if !(url.starts_with("https://") || url.starts_with("http://")) {
            anyhow::bail!("Only http:// or https:// URLs are allowed");
        }

        if self.allowed_domains.is_empty() && self.allowed_private_hosts.is_empty() {
            anyhow::bail!(
                "Browser tool is enabled but no allowed_domains are configured. Add [browser].allowed_domains in config.toml"
            );
        }

        let host = extract_host(url)?;
        let private_host = domain_guard::is_private_or_local_host(&host);
        let private_host_allowed = private_host
            && domain_guard::host_matches_allowlist(&host, &self.allowed_private_hosts);

        if private_host && !private_host_allowed {
            anyhow::bail!("Blocked local/private host: {host}");
        }

        if private_host_allowed {
            return Ok(url.to_string());
        }

        if !domain_guard::host_matches_allowlist(&host, &self.allowed_domains) {
            anyhow::bail!("Host '{host}' is not in browser.allowed_domains");
        }

        Ok(url.to_string())
    }
}

#[async_trait]
impl Tool for BrowserOpenTool {
    fn name(&self) -> &str {
        "browser_open"

View on GitHub (pinned to 88bb9c8533)

Solutions

  1. Add the exact host to [browser].allowed_private_hosts in config.toml (e.g. "localhost", "192.168.1.10") and restart
  2. Or use the service's public hostname, which then goes through the normal allowed_domains check
  3. Do not work around by adding broad private ranges unless you accept the SSRF exposure

Example fix

# config.toml — before
[browser]
allowed_domains = ["example.com"]

# after
[browser]
allowed_domains = ["example.com"]
allowed_private_hosts = ["localhost", "127.0.0.1"]
Defensive patterns

Strategy: validation

Validate before calling

fn looks_private(host: &str) -> bool {
    host == "localhost"
        || host.starts_with("127.")
        || host.starts_with("10.")
        || host.starts_with("192.168.")
        || host.starts_with("169.254.")
        || host.ends_with(".local")
}
if looks_private(&host) && !allowed_private_hosts.contains(&host.to_string()) {
    return Err(format!("private host '{host}' blocked; add it to allowed_private_hosts"));
}

Try / catch

match open_tool.execute(args).await {
    Ok(res) if res.success => { /* ... */ }
    Ok(res) => {
        if res.error.as_deref().unwrap_or_default().contains("Blocked local/private host") {
            // SSRF guard: add the exact host to allowed_private_hosts or use a public hostname
        }
    }
    Err(e) => return Err(e),
}

Prevention

When it happens

Trigger: Opening http://localhost:8080, http://127.0.0.1:9090, http://192.168.1.10/ui, or http://myserver.local when the host is not present in [browser].allowed_private_hosts.

Common situations: Developers wanting the agent to open a local admin UI or dev server; home-lab hostnames under .local; cloud metadata-style addresses (169.254.169.254) that the guard intentionally blocks as SSRF.

Related errors


AI-assisted analysis of zeroclaw-labs/zeroclaw@88bb9c8533 (2026-08-23). Data as JSON: /api/errors/ffa98dce2eff40bd. Report an issue: GitHub.