{"record":{"id":"550e120ee8d078b7","repo":"zeroclaw-labs/zeroclaw","slug":"screenshot-path-path-resolves-to-a-non-utf","errorCode":null,"errorMessage":"Screenshot path '{ $path }' resolves to a non-UTF-8 pathname; refusing to write through a lossy conversion","messagePattern":"Screenshot path '(.+?)' resolves to a non-UTF-8 pathname; refusing to write through a lossy conversion","errorType":"validation","errorClass":"anyhow::Error","httpStatus":null,"severity":"error","filePath":"crates/zeroclaw-tools/src/browser.rs","lineNumber":939,"sourceCode":"                \"tool-browser-screenshot-error-symlink-target\",\n                &[(\"target\", &resolved_target.display().to_string())],\n            );\n            anyhow::bail!(\"{msg}\");\n        }\n\n        // The allowlist above validated the byte-preserving PathBuf. Every\n        // backend receives the destination as a UTF-8 string, and a lossy\n        // conversion (`to_string_lossy`) would silently replace non-UTF-8\n        // bytes with U+FFFD — naming a pathname that never passed the policy.\n        // Fail closed here, while we still hold the checked target: on Unix a\n        // valid UTF-8 input can canonicalize (through a symlink) to a parent\n        // containing non-UTF-8 bytes.\n        let Some(resolved_str) = resolved_target.to_str() else {\n            let msg = crate::i18n::get_required_tool_string_with_args(\n                \"tool-browser-screenshot-error-path-not-utf8\",\n                &[(\"path\", raw_path)],\n            );\n            anyhow::bail!(\"{msg}\");\n        };\n\n        Ok(resolved_str.to_string())\n    }\n\n    fn validate_computer_use_action(\n        &self,\n        action: &str,\n        params: &serde_json::Map<String, Value>,\n    ) -> anyhow::Result<()> {\n        match action {\n            \"open\" => {\n                let url = params.get(\"url\").and_then(Value::as_str).ok_or_else(|| {\n                    ::zeroclaw_log::record!(\n                        WARN,\n                        ::zeroclaw_log::Event::new(module_path!(), ::zeroclaw_log::Action::Reject)\n                            .with_outcome(::zeroclaw_log::EventOutcome::Failure),\n                        \"browser: Missing 'url' for open action\"","sourceCodeStart":921,"sourceCodeEnd":957,"githubUrl":"https://github.com/zeroclaw-labs/zeroclaw/blob/88bb9c8533fc57ed7a03e36ca7c9ed2bf8336dcc/crates/zeroclaw-tools/src/browser.rs#L921-L957","documentation":"The allowlist validated the byte-preserving PathBuf, but every screenshot backend consumes the destination as a UTF-8 string. If the canonical target contains non-UTF-8 bytes (possible on Unix when a symlinked parent canonicalizes into a legacy-encoded directory), to_str() returns None and the tool refuses rather than applying to_string_lossy(), which would substitute U+FFFD and name a pathname that never passed policy.","triggerScenarios":"A syntactically valid UTF-8 path whose canonical parent directory contains non-UTF-8 bytes — typically reached through a symlink whose target is a Latin-1/ISO-8859-1 named directory, common in old home directories or migrated filesystems.","commonSituations":"Workspaces nested under legacy locale-encoded directories on older Linux/macOS setups; SMB/NFS mounts with mixed-encoding filenames; symlinks created by migration tooling pointing at byte-named targets.","solutions":["Rename the non-UTF-8 directory component to a valid UTF-8 name (convmv or manual rename)","Choose a screenshot destination inside a fully UTF-8 directory tree within the workspace","Remove or retarget the symlink whose canonicalization introduces the non-UTF-8 bytes"],"exampleFix":"# before: the workspace parent resolves (via symlink) to a Latin-1 named directory\n{\"action\": \"screenshot\", \"path\": \"shot.png\"}\n# after: write inside a guaranteed UTF-8 directory\n{\"action\": \"screenshot\", \"path\": \"captures/shot.png\"}","handlingStrategy":"validation","validationCode":"let canonical = tokio::fs::canonicalize(parent).await?;\nlet target = canonical.join(file_name);\nif target.to_str().is_none() {\n    // canonical destination is not UTF-8; reject before invoking the tool\n}","typeGuard":null,"tryCatchPattern":"match tool.execute(args).await {\n    Ok(res) => { /* ... */ }\n    Err(e) if e.to_string().contains(\"non-UTF-8\") => {\n        // pick a destination inside a UTF-8 directory tree; do not retry as-is\n    }\n    Err(e) => return Err(e),\n}","preventionTips":["Keep workspace paths pure ASCII","Rename legacy-encoded directories before pointing tools at them","Avoid symlinks whose targets lead outside a known UTF-8 tree"],"tags":["browser","screenshot","utf-8","filesystem","security"],"backgroundTag":"non-utf8-filename","analyzedSha":"88bb9c8533fc57ed7a03e36ca7c9ed2bf8336dcc","analyzedAt":"2026-08-23T01:07:41.857Z","schemaVersion":2},"datasetVersion":"2026-08-23T08:06:27.607Z"}