BloopAI/vibe-kanban · error · std::io::Error
cannot atomically write path without a parent directory
Error message
cannot atomically write path without a parent directory
What it means
atomic_write_text_file writes a file by creating a temp file in the path's parent directory and renaming it into place. Path::parent() returns None only for paths with no directory component (e.g. a bare relative filename like "config"), so this error signals a malformed destination path, not a missing directory (that's handled by create_dir_all).
Source
Thrown at crates/desktop-bridge/src/ssh_config.rs:117
if existing.contains(include_line) {
return Ok(());
}
// Prepend the Include directive (SSH config is first-match)
let new_content = format!("{include_line}\n{existing}");
atomic_write_text_file(&config_path, &new_content)?;
Ok(())
}
fn vk_ssh_dir() -> Result<PathBuf, DesktopBridgeError> {
let home = dirs::home_dir().ok_or(DesktopBridgeError::NoHomeDirectory)?;
Ok(home.join(".vk-ssh"))
}
fn atomic_write_text_file(path: &Path, content: &str) -> Result<(), DesktopBridgeError> {
let parent = path.parent().ok_or_else(|| {
io::Error::new(
io::ErrorKind::InvalidInput,
"cannot atomically write path without a parent directory",
)
})?;
fs::create_dir_all(parent)?;
let nonce = SystemTime::now()
.duration_since(UNIX_EPOCH)
.map(|d| d.as_nanos())
.unwrap_or(0);
let file_name = path
.file_name()
.and_then(|n| n.to_str())
.unwrap_or("config");
let tmp_name = format!(".{file_name}.tmp-{}-{nonce}", std::process::id());
let tmp_path = parent.join(tmp_name);
let mut tmp_file = fs::OpenOptions::new()View on GitHub (pinned to 4deb7eca8f)
Solutions
- Pass an absolute path that includes a directory component, e.g. ~/.vk-ssh/<file>.
- Check any env var or setting overriding the config location — an empty value collapses the path to a bare filename.
- Ensure dirs::home_dir() is resolving (the same function errors earlier with NoHomeDirectory if not).
- Build the path via join on a base directory rather than concatenating strings.
Example fix
// before
atomic_write_text_file(Path::new("vk-ssh-config"), content)?; // no parent
// after
let path = dirs::home_dir().unwrap().join(".vk-ssh").join("vk-ssh-config");
atomic_write_text_file(&path, content)?; Defensive patterns
Strategy: validation
Validate before calling
fn has_parent(path: &Path) -> bool {
path.parent().map_or(false, |p| !p.as_os_str().is_empty())
}
if !has_parent(&config_path) {
anyhow::bail!("config path must include a directory: {}", config_path.display());
} Type guard
fn is_writable_path(path: &Path) -> bool {
path.parent().map_or(false, |p| !p.as_os_str().is_empty())
} Try / catch
match update_ssh_config(...) {
Err(DesktopBridgeError::Io(e)) if e.kind() == ErrorKind::InvalidInput
&& e.to_string().contains("parent directory") =>
eprintln!("provide an absolute path with a directory component"),
other => other,
} Prevention
- Always build ssh config paths with PathBuf::join on a base directory.
- Validate any env var overriding the config location is non-empty and absolute.
- Never pass bare filenames to atomic-write helpers.
When it happens
Trigger: Calling update_ssh_config or ensure_ssh_include when a computed destination path is a bare filename with no parent component (e.g. passing "vk-ssh-config" instead of "/home/user/.vk-ssh/vk-ssh-config").
Common situations: Overriding the home directory lookup so config_dir() returns ""; passing a relative single-segment path through custom configuration; constructing paths from an empty string env var.
Related errors
- Failed to get relative path for {source_file:?}: {e}
- Copy project files timed out after 30s
- Copy files task failed: {e}
- File {source_file:?} is outside project directory
- Migration failed: {}
AI-assisted analysis of BloopAI/vibe-kanban@4deb7eca8f (2026-08-29).
Data as JSON: /api/errors/f1adf901fde4c58d.
Report an issue: GitHub.