{"record":{"id":"eb8f12d2c1136ebb","repo":"zeroclaw-labs/zeroclaw","slug":"cannot-write-screenshot-to-runtime-config-path","errorCode":null,"errorMessage":"Cannot write screenshot to runtime config path '{ $target }'","messagePattern":"Cannot write screenshot to runtime config path '(.+?)'","errorType":"validation","errorClass":"anyhow::Error","httpStatus":null,"severity":"error","filePath":"crates/zeroclaw-tools/src/browser.rs","lineNumber":913,"sourceCode":"        // the same target-level guards the file_write / file_edit tools use.\n        let Some(file_name) = full.file_name() else {\n            let msg = crate::i18n::get_required_tool_string_with_args(\n                \"tool-browser-screenshot-error-missing-filename\",\n                &[(\"path\", raw_path)],\n            );\n            anyhow::bail!(\"{msg}\");\n        };\n        let resolved_target = canonical.join(file_name);\n\n        if self.security.is_runtime_config_path(&resolved_target) {\n            let msg = crate::i18n::get_required_tool_string_with_args(\n                \"tool-browser-screenshot-error-runtime-config-target\",\n                &[\n                    (\"path\", raw_path),\n                    (\"target\", &resolved_target.display().to_string()),\n                ],\n            );\n            anyhow::bail!(\"{msg}\");\n        }\n\n        // If the target already exists and is a symlink, refuse to follow it.\n        if let Ok(meta) = tokio::fs::symlink_metadata(&resolved_target).await\n            && meta.file_type().is_symlink()\n        {\n            let msg = crate::i18n::get_required_tool_string_with_args(\n                \"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","sourceCodeStart":895,"sourceCodeEnd":931,"githubUrl":"https://github.com/zeroclaw-labs/zeroclaw/blob/88bb9c8533fc57ed7a03e36ca7c9ed2bf8336dcc/crates/zeroclaw-tools/src/browser.rs#L895-L931","documentation":"The canonical screenshot-destination validator resolves the requested path against the workspace (tilde and `..` expanded, parent canonicalized) and then applies the same runtime-config guard used by file_write/file_edit: a target that is the workspace's config.toml, config.toml.bak, or a `.config.toml.tmp-*` temp file is refused. This stops a screenshot write from overwriting the agent's own live configuration. The error is raised before any backend writes a byte.","triggerScenarios":"Invoking the browser tool with action=\"screenshot\" and a path whose resolved target lands on the runtime config set: path=\"config.toml\", path=\"config.toml.bak\", path=\".config.toml.tmp-123\", or a relative/tilde path that canonicalizes onto one of these after parent resolution.","commonSituations":"An LLM agent told to 'save a screenshot here' picks the most prominent file in the workspace root (config.toml); users assume every workspace path is writable by every tool; paths that look harmless but resolve through `..` or tilde expansion onto the config file.","solutions":["Pick a different destination filename, e.g. screenshots/capture-01.png","If the goal is to change configuration, use the dedicated config-editing path (file_edit tool or config workflow), not a screenshot write","Read the {target} field in the message to see the exact resolved path that tripped the guard, then remove the offending segment (`..`, tilde, or symlink) from the requested path"],"exampleFix":"// before\n{\"action\": \"screenshot\", \"path\": \"config.toml\"}\n// after\n{\"action\": \"screenshot\", \"path\": \"screenshots/capture-01.png\"}","handlingStrategy":"validation","validationCode":"fn is_runtime_config_name(name: &str) -> bool {\n    name == \"config.toml\"\n        || name == \"config.toml.bak\"\n        || name.starts_with(\".config.toml.tmp-\")\n}\n\nlet file = std::path::Path::new(&requested_path)\n    .file_name().and_then(|n| n.to_str()).unwrap_or(\"\");\nif is_runtime_config_name(file) { /* choose another destination before calling */ }","typeGuard":null,"tryCatchPattern":"match tool.execute(args).await {\n    Ok(res) => { /* ... */ }\n    Err(e) if e.to_string().contains(\"runtime config\") => {\n        // policy denial: do NOT retry the same path; pick a new filename\n    }\n    Err(e) => return Err(e),\n}","preventionTips":["Default screenshot destinations to a dedicated screenshots/ subdirectory","Treat path-policy errors as non-retryable — the identical path always fails again","Never point output-writing tools at workspace config files"],"tags":["browser","screenshot","path-validation","security","config-protection"],"backgroundTag":"config-file-write-protected","analyzedSha":"88bb9c8533fc57ed7a03e36ca7c9ed2bf8336dcc","analyzedAt":"2026-08-23T01:07:41.857Z","schemaVersion":2},"datasetVersion":"2026-08-23T08:06:27.607Z"}