{"record":{"id":"4a50e956177092d2","repo":"zeroclaw-labs/zeroclaw","slug":"slack-outbound-attachment-target-is-not-a-file","errorCode":null,"errorMessage":"Slack outbound attachment target is not a file: {}","messagePattern":"Slack outbound attachment target is not a file: (.+?)","errorType":"validation","errorClass":"anyhow::Error","httpStatus":null,"severity":"error","filePath":"crates/zeroclaw-channels/src/slack.rs","lineNumber":1069,"sourceCode":"            .with_context(|| format!(\"Slack outbound attachment path not found: {target}\"))?;\n\n        if !canonical_path.starts_with(&canonical_workspace) {\n            anyhow::bail!(\n                \"Slack outbound attachment path escapes workspace: {}\",\n                canonical_path.display()\n            );\n        }\n\n        let metadata = tokio::fs::metadata(&canonical_path)\n            .await\n            .with_context(|| {\n                format!(\n                    \"failed to stat Slack outbound attachment {}\",\n                    canonical_path.display()\n                )\n            })?;\n        if !metadata.is_file() {\n            anyhow::bail!(\n                \"Slack outbound attachment target is not a file: {}\",\n                canonical_path.display()\n            );\n        }\n        if metadata.len() > SLACK_OUTBOUND_ATTACHMENT_MAX_BYTES as u64 {\n            anyhow::bail!(\n                \"Slack outbound attachment exceeds {} bytes: {}\",\n                SLACK_OUTBOUND_ATTACHMENT_MAX_BYTES,\n                canonical_path.display()\n            );\n        }\n\n        let data = tokio::fs::read(&canonical_path).await.with_context(|| {\n            format!(\n                \"failed to read Slack outbound attachment {}\",\n                canonical_path.display()\n            )\n        })?;","sourceCodeStart":1051,"sourceCodeEnd":1087,"githubUrl":"https://github.com/zeroclaw-labs/zeroclaw/blob/88bb9c8533fc57ed7a03e36ca7c9ed2bf8336dcc/crates/zeroclaw-channels/src/slack.rs#L1051-L1087","documentation":"Raised by SlackChannel::resolve_outbound_attachment_marker when tokio::fs::metadata succeeds on the canonicalized target but metadata.is_file() is false — the target is a directory, fifo, device node, or other non-regular file. It fires after the workspace containment check and before the size check against SLACK_OUTBOUND_ATTACHMENT_MAX_BYTES (20 MiB), so the marker target must be a readable regular file inside the workspace.","triggerScenarios":"An outbound attachment marker points at a directory (e.g. [file:/workspace/reports]) or a special file; the stat succeeds, is_file() is false, and the bail includes the canonical path.","commonSituations":"Agents referencing a folder of exports instead of a specific file; markers generated from a path where the file was expected but a directory was created; trailing-slash or empty-name paths resolving to a directory.","solutions":["Point the marker at a specific regular file inside the workspace","If the intent was 'attach everything in this directory', enumerate the files and emit one marker per file","Generate the asset fully (close/flush writers) before the marker is resolved"],"exampleFix":null,"handlingStrategy":"validation","validationCode":"async fn attachment_is_sendable(target: &str) -> bool {\n    match tokio::fs::metadata(target).await {\n        Ok(m) => m.is_file() && m.len() <= 20 * 1024 * 1024, // regular file, <= SLACK_OUTBOUND_ATTACHMENT_MAX_BYTES\n        Err(_) => false,\n    }\n}","typeGuard":null,"tryCatchPattern":null,"preventionTips":["Stat the target before sending: must be a regular file, not a directory or special file","Point markers at concrete generated files, never at output directories","Also check the 20 MiB size cap up front — the adjacent guard rejects oversized files after this one"],"tags":["slack","attachment","not-a-file","validation","fs","workspace"],"backgroundTag":"not-a-regular-file","analyzedSha":"88bb9c8533fc57ed7a03e36ca7c9ed2bf8336dcc","analyzedAt":"2026-08-23T01:07:41.857Z","schemaVersion":2},"datasetVersion":"2026-08-23T08:06:27.607Z"}