zeroclaw-labs/zeroclaw · error · anyhow::Error
attachment path not found: {}
Error message
attachment path not found: {} What it means
`load_attachment_payload` resolved a local attachment target to a workspace path successfully (the sandbox check passed — note `canonicalize_within_workspace` deliberately returns non-existent candidates unchanged), but the path does not exist on disk at send time. The message prints the fully resolved absolute path so you can see exactly where the channel looked.
Source
Thrown at crates/zeroclaw-channels/src/wechat.rs:1231
file_name: self.remote_file_name(url, content_type.as_deref(), kind),
bytes,
})
}
async fn load_attachment_payload(
&self,
attachment: &WeChatAttachment,
) -> anyhow::Result<WeChatMediaPayload> {
let target = attachment.target.trim();
if is_remote_url(target) {
return self
.download_remote_attachment(target, attachment.kind)
.await;
}
let path = self.resolve_local_attachment_path(target)?;
if !path.exists() {
anyhow::bail!("attachment path not found: {}", path.display());
}
let file_name = sanitize_attachment_filename(
path.file_name()
.and_then(|name| name.to_str())
.unwrap_or("attachment.bin"),
)
.unwrap_or_else(|| {
format!(
"wechat_attachment_{}.{}",
uuid::Uuid::new_v4().simple(),
attachment.kind.default_extension()
)
});
let bytes = tokio::fs::read(&path)
.await
.with_context(|| format!("attachment read failed: {}", path.display()))?;View on GitHub (pinned to 88bb9c8533)
Solutions
- Check the printed resolved path: compare it with where the file actually is, then fix the target (usually switch to the correct workspace-relative name).
- If the file lives elsewhere, move/copy it into `workspace_dir` first, or fix the `workspace_dir` channel configuration to the real workspace root.
- Guard against races: write attachment files to stable paths and send immediately, or verify existence right before send.
- For files produced by external processes, pass absolute paths that are already inside the workspace so resolution is unambiguous.
Example fix
// before: assuming the renderer's output path is the attachment target
let attachment = WeChatAttachment { target: "/tmp/chart.png".into(), .. }; // resolves? no: /tmp is outside workspace
let attachment = WeChatAttachment { target: "char.png".into(), .. }; // typo -> attachment path not found: /workspace/char.png
// after: write into the workspace and verify before sending
let path = workspace.join("chart.png");
renderer.render(&path).await?;
assert!(path.exists(), "renderer produced no output");
let attachment = WeChatAttachment { target: "chart.png".into(), .. }; Defensive patterns
Strategy: validation
Validate before calling
// existence check inside the workspace before send
async fn attachment_ready(workspace: &std::path::Path, rel: &str) -> bool {
let p = workspace.join(rel);
p.is_file() // resolves symlinks like the channel will
}
if !attachment_ready(workspace, "chart.png").await {
anyhow::bail!("attachment missing before send: {rel}");
} Type guard
fn is_existing_attachment(target: &str, workspace: &std::path::Path) -> bool {
let p = std::path::Path::new(target.trim().strip_prefix("file://").unwrap_or(target.trim()));
let resolved = if p.is_absolute() { p.to_path_buf() } else { workspace.join(p) };
resolved.is_file()
} Try / catch
match channel.send(msg_with_attachment(rel)).await {
Err(err) if err.to_string().contains("attachment path not found") => {
let abs = err.to_string(); // message contains the resolved absolute path
tracing::warn!(%abs, "attachment vanished or misnamed; regenerating");
regenerate_attachment(workspace, rel).await?;
channel.send(msg_with_attachment(rel)).await?;
}
other => other?,
} Prevention
- Have producers return the final workspace-relative path and use exactly that string as the target (no manual retyping of filenames).
- Verify existence right before send when files are cleaned up concurrently (temp scrubbers, CI artifact pruning).
- In containers, mount the artifact directory and workspace_dir at identical paths so names resolve the same everywhere.
- Watch for extension/case mismatches and URL-encoded characters in generated filenames.
When it happens
Trigger: Sending a WeChat attachment whose target resolves inside `workspace_dir` but names a missing file: wrong filename/extension, file deleted between generation and send, attachment path produced by another machine/container, or the workspace root mounted at a different location. Fires for relative targets (`report.png` when `/workspace/report.png` is absent), `/workspace/...` targets, and in-workspace absolute targets alike.
Common situations: Agent writes a file to a temp directory and passes that name as if it were in the workspace; filename mismatches (`.jpeg` vs `.jpg`, URL-encoded or spaced names); race where cleanup deletes the artifact before the send; Docker deployments where the generator and the channel see different volumes; typo'd attachment targets in message templates.
Understand the failure class
Background: "File not found" and ENOENT errors: why libraries can't find a file that should exist — this error's family across 50 libraries.
Related errors
- attachment path {} canonicalizes to {} which escapes workspa
- attachment path {} escapes workspace {}
- Slack outbound attachment target must be a local workspace p
- Slack outbound attachment path must be absolute: {target}
- Slack outbound attachment path escapes workspace: {}
AI-assisted analysis of zeroclaw-labs/zeroclaw@88bb9c8533 (2026-08-23).
Data as JSON: /api/errors/5a18d812b6907de5.
Report an issue: GitHub.