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
- Copy the file to a new name and edit the copy, breaking the extra link (as the message advises).
- Remove the other hard links to the inode, then retry the write.
- 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
- Never hard-link files inside a workspace; use copies or symlinks.
- Check st_nlink before editing files that may come from link-based backups.
- Educate users that ln (not -s) into workspaces blocks atomic writes.
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
- could not inspect
- could not inspect
- could not read
- {error}
- external credential path must name a regular file
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)