zeroclaw-labs/zeroclaw · error
domain '{}' is not in browser_delegate allowed_domains
Error message
domain '{}' is not in browser_delegate allowed_domains What it means
When browser_delegate's allowed_domains is non-empty it acts as a strict allowlist: the URL's domain must match an entry exactly or as a subdomain (domain_matches: equal, or ends with ".pattern"), otherwise the task is rejected. An empty allowed_domains list means no restriction beyond the denylist.
Source
Thrown at crates/zeroclaw-tools/src/browser_delegate.rs:114
anyhow::bail!("URL has no host: {}", url);
}
// Check blocked domains first (deny takes precedence)
for blocked in &self.config.blocked_domains {
if domain_matches(&domain, blocked) {
anyhow::bail!("domain '{}' is blocked by browser_delegate policy", domain);
}
}
// If allowed_domains is non-empty, it acts as an allowlist
if !self.config.allowed_domains.is_empty() {
let allowed = self
.config
.allowed_domains
.iter()
.any(|d| domain_matches(&domain, d));
if !allowed {
anyhow::bail!(
"domain '{}' is not in browser_delegate allowed_domains",
domain
);
}
}
Ok(())
}
}
/// Check whether `domain` matches a pattern (exact or suffix match).
fn domain_matches(domain: &str, pattern: &str) -> bool {
let d = domain.to_lowercase();
let p = pattern.to_lowercase();
d == p || d.ends_with(&format!(".{}", p))
}
/// Maximum stderr bytes to capture from the subprocess.View on GitHub (pinned to 88bb9c8533)
Solutions
- Add the exact domain (or its parent) to browser_delegate allowed_domains in config
- Verify the matching semantics: suffix matching covers subdomains only, never sibling TLDs
- Keep the allowlist maintained alongside the services the agent is authorized to touch
Example fix
# config.toml — before [browser_delegate] allowed_domains = ["example.com"] # after allowed_domains = ["example.com", "api.example.org"]
Defensive patterns
Strategy: validation
Validate before calling
fn domain_matches(domain: &str, pattern: &str) -> bool {
let (d, p) = (domain.to_lowercase(), pattern.to_lowercase());
d == p || d.ends_with(&format!(".{p}"))
}
let allowed = allowed_domains.iter().any(|p| domain_matches(&host, p));
if !allowed_domains.is_empty() && !allowed {
return Err(format!("host '{host}' not allowlisted"));
} Try / catch
match delegate.execute(args).await {
Ok(res) if res.success => { /* ... */ }
Ok(res) => {
if res.error.as_deref().unwrap_or_default().contains("not in browser_delegate allowed_domains") {
// add the domain to config or drop the URL from the task
}
}
Err(e) => return Err(e),
} Prevention
- Keep the allowlist in sync with the services the agent may access
- Remember matching is exact-or-subdomain; sibling TLDs need their own entries
- Lowercase domains in config to avoid case-mismatch surprises
When it happens
Trigger: A task referencing https://api.example.org while allowed_domains=["example.com"] — example.org does not match; sibling domains and different TLDs never match a parent entry.
Common situations: Forgetting the allowlist is exact-or-child-domain: docs.example.com passes with entry example.com, but example.co.uk does not; new microservices on separate domains need config updates; typos in either the URL or the config entry.
Related errors
- domain '{}' is blocked by browser_delegate policy
- Host '{host}' is not in browser.allowed_domains
- Workspace path {} is not in runtime.docker.allowed_workspace
- Serial path not allowed: {}. Allowed: {}
- memory write blocked by content scan: {kinds}
AI-assisted analysis of zeroclaw-labs/zeroclaw@88bb9c8533 (2026-08-23).
Data as JSON: /api/errors/75329a756f1bbab6.
Report an issue: GitHub.