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
- Add the exact host to [browser].allowed_private_hosts in config.toml (e.g. "localhost", "192.168.1.10") and restart
- Or use the service's public hostname, which then goes through the normal allowed_domains check
- 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
- List only the exact local hosts needed in allowed_private_hosts
- Prefer public hostnames for services the agent must open
- Never add broad private ranges (e.g. whole 10.0.0.0/8) casually
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
- Host '{host}' is not in browser.allowed_domains
- Blocked local/private host: {host}
- Blocked local/private host: {display_host}
- PermissionDenied
- Workspace path {} is not in runtime.docker.allowed_workspace
AI-assisted analysis of zeroclaw-labs/zeroclaw@88bb9c8533 (2026-08-23).
Data as JSON: /api/errors/ffa98dce2eff40bd.
Report an issue: GitHub.