{"record":{"id":"eb424d225d7ea83c","repo":"Hmbown/CodeWhale","slug":"refusing-to-rewrite-the-file-has-links-hard-links-and-path","errorCode":null,"errorMessage":"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","messagePattern":"refusing to rewrite (.+?): the file has (.+?) hard links and path checks cannot prove the other links stay inside the workspace; copy it to a new name to break the link","errorType":"validation","errorClass":"io::Error","httpStatus":null,"severity":"error","filePath":"crates/tui/src/utils.rs","lineNumber":289,"sourceCode":"///   (same candidate mode as ordinary `std::fs::write`).\n/// - Existing files keep ordinary permission bits (`mode & 0o777`), including\n///   executable bits. setuid/setgid/sticky are intentionally not restored.\n///\n/// On Windows this matches [`write_atomic`] (no POSIX mode simulation).\n///\n/// # Errors\n/// Same failure modes as [`write_atomic`].\npub fn write_atomic_workspace(path: &Path, contents: &[u8]) -> std::io::Result<()> {\n    // Hard-link guard (issue #5569): a workspace path that shares its inode\n    // with another name cannot be proven to stay inside the writable root by\n    // path checks. Atomic rename would replace the directory entry (leaving\n    // the outside link on the old inode), but that silently splits the pair\n    // and would not block a future non-atomic writer. Fail closed on both\n    // platforms that can count links.\n    if let Some(links) = hard_link_count(path)\n        && links > 1\n    {\n        return Err(std::io::Error::new(\n            std::io::ErrorKind::InvalidData,\n            format!(\n                \"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\",\n                path.display(),\n            ),\n        ));\n    }\n    write_atomic_with_permissions(path, contents, AtomicWritePermissions::Workspace)\n}\n\n/// Hard-link count for an existing regular file, or `None` when the platform\n/// cannot answer or the path is not a regular file.\n///\n/// `None` means \"unknown\", never \"one\". A caller guarding against link\n/// escapes must treat an unknown count as unguarded, not as safe.\n#[cfg(unix)]\nfn hard_link_count(path: &Path) -> Option<u64> {\n    let metadata = std::fs::metadata(path).ok()?;","sourceCodeStart":271,"sourceCodeEnd":307,"githubUrl":"https://github.com/Hmbown/CodeWhale/blob/73e0f67d83c59909b571efdfc88c4bc28c309cb1/crates/tui/src/utils.rs#L271-L307","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","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)."],"exampleFix":"// before: file has 2 links\nwrite_atomic_workspace(\"data/config.toml\", &bytes)?;\n// after\ncp data/config.toml data/config.toml.new   # breaks the link\nmv data/config.toml.new data/config.toml\nwrite_atomic_workspace(\"data/config.toml\", &bytes)?;","handlingStrategy":"validation","validationCode":"let links = std::fs::metadata(path)?.nlink();\nif links > 1 { /* copy to new name first */ }","typeGuard":null,"tryCatchPattern":"match write_atomic_workspace(path, &bytes) {\n    Err(e) if e.kind() == io::ErrorKind::InvalidData && e.to_string().contains(\"hard links\") => {\n        break_hard_link(path)?;\n        write_atomic_workspace(path, &bytes)?;\n    }\n    r => r,\n}","preventionTips":["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."],"tags":["filesystem","io","safety"],"backgroundTag":"file-write-failed","analyzedSha":"73e0f67d83c59909b571efdfc88c4bc28c309cb1","analyzedAt":"2026-09-22T01:30:00.501Z","contentChangedAt":"2026-09-22T01:30:00.501Z","schemaVersion":2},"datasetVersion":"2026-09-23T08:17:48.524Z"}