{"record":{"id":"c2d2a4c1a88b33db","repo":"zeroclaw-labs/zeroclaw","slug":"slack-outbound-attachment-target-must-be-a-local-w","errorCode":null,"errorMessage":"Slack outbound attachment target must be a local workspace path","messagePattern":"Slack outbound attachment target must be a local workspace path","errorType":"validation","errorClass":"anyhow::Error","httpStatus":null,"severity":"error","filePath":"crates/zeroclaw-channels/src/slack.rs","lineNumber":1031,"sourceCode":"        Ok(())\n    }\n\n    fn http_client(&self) -> reqwest::Client {\n        zeroclaw_config::schema::build_channel_proxy_client_with_timeouts(\n            \"channel.slack\",\n            self.proxy_url.as_deref(),\n            30,\n            10,\n        )\n    }\n\n    async fn resolve_outbound_attachment_marker(\n        &self,\n        marker: &SlackOutboundAttachmentMarker,\n    ) -> anyhow::Result<MediaAttachment> {\n        let target = marker.target.trim();\n        if target.starts_with(\"file:\") || target.starts_with(\"data:\") || target.contains(\"://\") {\n            anyhow::bail!(\"Slack outbound attachment target must be a local workspace path\");\n        }\n\n        let path = Path::new(target);\n        if !path.is_absolute() {\n            anyhow::bail!(\"Slack outbound attachment path must be absolute: {target}\");\n        }\n\n        let workspace = self\n            .workspace_dir\n            .as_deref()\n            .context(\"Slack outbound local attachments require workspace_dir\")?;\n        let canonical_workspace = tokio::fs::canonicalize(workspace).await.with_context(|| {\n            format!(\n                \"failed to canonicalize Slack workspace {}\",\n                workspace.display()\n            )\n        })?;\n        let canonical_path = tokio::fs::canonicalize(path)","sourceCodeStart":1013,"sourceCodeEnd":1049,"githubUrl":"https://github.com/zeroclaw-labs/zeroclaw/blob/88bb9c8533fc57ed7a03e36ca7c9ed2bf8336dcc/crates/zeroclaw-channels/src/slack.rs#L1013-L1049","documentation":"Raised by SlackChannel::resolve_outbound_attachment_marker when the target of a parsed outbound attachment marker ([image:...] or [file:...] in outgoing message text) starts with 'file:', 'data:', or contains '://'. This is a deliberate security guard: Slack outbound attachments must reference local workspace files, never URLs or inline data, because the channel uploads the file bytes itself after canonicalizing them inside workspace_dir.","triggerScenarios":"Outgoing message text contains a marker like [image:https://cdn.example.com/pic.png] or [file:data:text/plain;base64,...]; parse_outbound_attachment_markers extracts it and resolve_outbound_attachment_marker rejects the target scheme before any path work.","commonSituations":"An agent or template emits remote URLs or data URIs in attachment markers, expecting them to be forwarded as-is; porting behavior from another channel that accepted URLs; markdown link syntax colliding with the marker grammar.","solutions":["Change the marker target to an absolute local workspace path and let Slack upload the bytes","If the content only exists remotely, download it into the workspace first, then reference the local path","Strip or rewrite malformed markers before sending if no upload is intended"],"exampleFix":"// before — marker points at a remote URL\nlet msg = \"report ready [image:https://cdn.example.com/chart.png]\";\n\n// after — download once, reference the local workspace file\nlet path = workspace_dir.join(\"attachments/chart.png\"); // populated beforehand\nlet msg = format!(\"report ready [image:{}]\", path.display());","handlingStrategy":"validation","validationCode":"fn sanitize_attachment_markers(msg: &str, workspace_dir: &Path) -> String {\n    // rewrite [image:<url>] / [file:<url>] markers into local workspace paths,\n    // dropping markers whose content cannot be downloaded\n    rewrite_markers(msg, |kind, target| {\n        if is_local_workspace_target(target) {\n            Some(format!(\"[{kind}:{target}]\"))\n        } else {\n            download_to_workspace(target, workspace_dir)\n                .ok()\n                .map(|p| format!(\"[{kind}:{}]\", p.display()))\n        }\n    })\n}","typeGuard":"fn is_local_workspace_target(target: &str) -> bool {\n    !(target.starts_with(\"file:\") || target.starts_with(\"data:\") || target.contains(\"://\"))\n}","tryCatchPattern":null,"preventionTips":["Validate marker targets with the same rule the channel uses (no file:, data:, or ://) before sending","Download remote assets into workspace_dir first and reference the local absolute path","When emitting markers from templates, only ever interpolate workspace-absolute paths"],"tags":["slack","attachment","validation","url-rejected","security-guard","workspace"],"backgroundTag":"path-validation-failed","analyzedSha":"88bb9c8533fc57ed7a03e36ca7c9ed2bf8336dcc","analyzedAt":"2026-08-23T01:07:41.857Z","schemaVersion":2},"datasetVersion":"2026-08-23T08:06:27.607Z"}