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

  1. Pass an absolute path that includes a directory component, e.g. ~/.vk-ssh/<file>.
  2. Check any env var or setting overriding the config location — an empty value collapses the path to a bare filename.
  3. Ensure dirs::home_dir() is resolving (the same function errors earlier with NoHomeDirectory if not).
  4. 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

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


AI-assisted analysis of BloopAI/vibe-kanban@4deb7eca8f (2026-08-29). Data as JSON: /api/errors/f1adf901fde4c58d. Report an issue: GitHub.