Hmbown/CodeWhale · error · io::Error

refusing to rewrite : the file has hard links and path…

Error message

refusing to rewrite {}: the file has {links} hard links and path checks cannot prove the other links stay inside the workspace; copy it to a new name to break the link

What it means

write_atomic_workspace refuses to replace a file that has more than one hard link. Atomic replacement (rename) would leave the other links pointing at the old inode, silently splitting the link pair and letting a future non-atomic writer bypass the workspace path checks, so it fails closed with ErrorKind::InvalidData.

Solutions

  1. Copy the file to a new name and edit the copy, breaking the extra link (as the message advises).
  2. Remove the other hard links to the inode, then retry the write.
  3. Replace the hard link with an independent copy in place (cp to temp, mv over).

Example fix

// before: file has 2 links
write_atomic_workspace("data/config.toml", &bytes)?;
// after
cp data/config.toml data/config.toml.new   # breaks the link
mv data/config.toml.new data/config.toml
write_atomic_workspace("data/config.toml", &bytes)?;
Defensive patterns

Strategy: validation

Validate before calling

let links = std::fs::metadata(path)?.nlink();
if links > 1 { /* copy to new name first */ }

Try / catch

match write_atomic_workspace(path, &bytes) {
    Err(e) if e.kind() == io::ErrorKind::InvalidData && e.to_string().contains("hard links") => {
        break_hard_link(path)?;
        write_atomic_workspace(path, &bytes)?;
    }
    r => r,
}

Prevention

When it happens

Trigger: Calling write_atomic_workspace on a path whose hard_link_count(path) returns links > 1 — i.e. the file shares its inode with at least one other directory entry.

Common situations: User hard-linked a workspace file (ln without -s) outside the workspace, backup tools creating hard-link snapshots (rsync --link-dest, Time Machine-style trees), build caches that hard-link outputs.

Understand the failure class

Background: "failed to write file", "Could not save figure", "Error saving remote file" — file write failed: causes and fixes across languages and libraries — this error's family across 38 libraries.

Related errors


AI-assisted analysis of Hmbown/CodeWhale@73e0f67d83 (2026-09-22). Data as JSON: /api/errors/eb424d225d7ea83c. Report an issue: GitHub.

Appendix: source

Thrown at crates/tui/src/utils.rs:289

///   (same candidate mode as ordinary `std::fs::write`).
/// - Existing files keep ordinary permission bits (`mode & 0o777`), including
///   executable bits. setuid/setgid/sticky are intentionally not restored.
///
/// On Windows this matches [`write_atomic`] (no POSIX mode simulation).
///
/// # Errors
/// Same failure modes as [`write_atomic`].
pub fn write_atomic_workspace(path: &Path, contents: &[u8]) -> std::io::Result<()> {
    // Hard-link guard (issue #5569): a workspace path that shares its inode
    // with another name cannot be proven to stay inside the writable root by
    // path checks. Atomic rename would replace the directory entry (leaving
    // the outside link on the old inode), but that silently splits the pair
    // and would not block a future non-atomic writer. Fail closed on both
    // platforms that can count links.
    if let Some(links) = hard_link_count(path)
        && links > 1
    {
        return Err(std::io::Error::new(
            std::io::ErrorKind::InvalidData,
            format!(
                "refusing to rewrite {}: the file has {links} hard links and path checks cannot prove the other links stay inside the workspace; copy it to a new name to break the link",
                path.display(),
            ),
        ));
    }
    write_atomic_with_permissions(path, contents, AtomicWritePermissions::Workspace)
}

/// Hard-link count for an existing regular file, or `None` when the platform
/// cannot answer or the path is not a regular file.
///
/// `None` means "unknown", never "one". A caller guarding against link
/// escapes must treat an unknown count as unguarded, not as safe.
#[cfg(unix)]
fn hard_link_count(path: &Path) -> Option<u64> {
    let metadata = std::fs::metadata(path).ok()?;

View on GitHub (pinned to 73e0f67d83)