{"record":{"id":"23a1af17669e6c6b","repo":"zeroclaw-labs/zeroclaw","slug":"domain-is-blocked-by-browser-delegate-policy","errorCode":null,"errorMessage":"domain '{}' is blocked by browser_delegate policy","messagePattern":"domain '(.+?)' is blocked by browser_delegate policy","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"error","filePath":"crates/zeroclaw-tools/src/browser_delegate.rs","lineNumber":102,"sourceCode":"            anyhow::Error::msg(format!(\"invalid URL '{}': {}\", url, e))\n        })?;\n\n        // Only allow http/https schemes\n        let scheme = parsed.scheme();\n        if scheme != \"http\" && scheme != \"https\" {\n            anyhow::bail!(\"unsupported URL scheme: {}\", scheme);\n        }\n\n        let domain = parsed.host_str().unwrap_or(\"\").to_string();\n\n        if domain.is_empty() {\n            anyhow::bail!(\"URL has no host: {}\", url);\n        }\n\n        // Check blocked domains first (deny takes precedence)\n        for blocked in &self.config.blocked_domains {\n            if domain_matches(&domain, blocked) {\n                anyhow::bail!(\"domain '{}' is blocked by browser_delegate policy\", domain);\n            }\n        }\n\n        // If allowed_domains is non-empty, it acts as an allowlist\n        if !self.config.allowed_domains.is_empty() {\n            let allowed = self\n                .config\n                .allowed_domains\n                .iter()\n                .any(|d| domain_matches(&domain, d));\n            if !allowed {\n                anyhow::bail!(\n                    \"domain '{}' is not in browser_delegate allowed_domains\",\n                    domain\n                );\n            }\n        }\n","sourceCodeStart":84,"sourceCodeEnd":120,"githubUrl":"https://github.com/zeroclaw-labs/zeroclaw/blob/88bb9c8533fc57ed7a03e36ca7c9ed2bf8336dcc/crates/zeroclaw-tools/src/browser_delegate.rs#L84-L120","documentation":"browser_delegate checks the blocked_domains denylist first, and a match aborts immediately — deny always wins, even when the domain is also allowlisted or the allowlist is empty. This is a hard policy boundary: the subprocess never receives tasks referencing blocked domains.","triggerScenarios":"A task URL (or a URL embedded in task text) whose domain matches a blocked_domains entry. Matching is exact or parent-domain: docs.example.com matches an entry of example.com.","commonSituations":"Policy configs blocking webmail, social, or internal domains; agents embedding a blocked link in otherwise benign task prose; stale config still blocking a domain a team now needs.","solutions":["If access is legitimately required, remove or narrow the entry in browser_delegate blocked_domains configuration","Otherwise drop the blocked URL from the task","Remember precedence: adding the domain to allowed_domains will NOT unblock it"],"exampleFix":"# config.toml — before\n[browser_delegate]\nblocked_domains = [\"example.com\"]\n\n# after (access granted deliberately)\nblocked_domains = []","handlingStrategy":"try-catch","validationCode":null,"typeGuard":null,"tryCatchPattern":"match delegate.execute(args).await {\n    Ok(res) if res.success => { /* ... */ }\n    Ok(res) => {\n        if res.error.as_deref().unwrap_or_default().contains(\"blocked by browser_delegate policy\") {\n            // policy denial: drop the blocked URL; do not retry or try to bypass\n        }\n    }\n    Err(e) => return Err(e),\n}","preventionTips":["Treat blocked_domains denials as final — allowlist entries cannot override them","Log denials so policy owners can audit and update the denylist deliberately","Review the denylist when onboarding new legitimate domains"],"tags":["browser-delegate","domain","denylist","policy","security"],"backgroundTag":"domain-blocked-by-policy","analyzedSha":"88bb9c8533fc57ed7a03e36ca7c9ed2bf8336dcc","analyzedAt":"2026-08-23T01:07:41.857Z","schemaVersion":2},"datasetVersion":"2026-08-23T08:06:27.607Z"}