Hmbown/CodeWhale · error

Automation lock must not have hard links

Error message

Automation lock must not have hard links

What it means

On Unix, after confirming the lock path is a regular file, the manager checks its hard-link count (st_nlink). A lock file with nlink != 1 means additional directory entries reference the same inode, which breaks the lock's uniqueness assumptions (another path could bypass or interfere with locking), so it bails.

Solutions

  1. Remove the extra hard links: find all links via `find -samefile <lockpath>` and delete the duplicates so nlink becomes 1
  2. Recreate the lock file: delete it and let the automation manager recreate it with a single link
  3. Configure backup/sync tools to copy instead of hard-link lock files

Example fix

// before
$ ln /run/app/automation.lock /run/app/automation.lock.bak   (nlink=2)
// after
$ rm /run/app/automation.lock.bak                            (nlink=1)
Defensive patterns

Strategy: validation

Validate before calling

#[cfg(unix)]
fn nlink_is_one(p: &std::path::Path) -> bool {
    use std::os::unix::fs::MetadataExt;
    std::fs::metadata(p).map(|m| m.nlink() == 1).unwrap_or(false)
}

Try / catch

match acquire_automation_lock(path) {
    Ok(g) => run(g),
    Err(e) => {
        eprintln!("Lock hard-linked: {e}; deleting lock and recreating");
        let _ = std::fs::remove_file(path);
        // retry acquisition once
    }
}

Prevention

When it happens

Trigger: Someone ran `ln <lockfile> <other-path>` creating a second hard link to the lock file, or a backup/sync tool hard-linked the lock.

Common situations: Dotfile managers (stow, rclone, backup restore) that hard-link files, users creating aliases to the lock file to 'share' state, or restores that re-link files.

Understand the failure class

Background: "This is a bug, please report it": internal invariant violations, unreachable panics, and SNH errors explained — this error's family across 47 libraries.

Related errors


AI-assisted analysis of Hmbown/CodeWhale@433685b202 (2026-09-15). Data as JSON: /api/errors/e200955f80186869. Report an issue: GitHub.

Appendix: source

Thrown at crates/tui/src/automation_manager.rs:1029

                .custom_flags(libc::O_NOFOLLOW | libc::O_CLOEXEC);
        }
        #[cfg(windows)]
        {
            use std::os::windows::fs::OpenOptionsExt as _;
            options.custom_flags(0x0020_0000); // FILE_FLAG_OPEN_REPARSE_POINT
        }
        let file = options
            .open(&path)
            .with_context(|| format!("open {}", path.display()))?;
        let metadata = file.metadata()?;
        if !metadata.is_file() {
            bail!("Automation lock must be a regular file");
        }
        #[cfg(unix)]
        {
            use std::os::unix::fs::MetadataExt as _;
            if metadata.nlink() != 1 {
                bail!("Automation lock must not have hard links");
            }
        }
        #[cfg(windows)]
        {
            use std::os::windows::fs::MetadataExt as _;
            if metadata.file_attributes() & 0x400 != 0 {
                bail!("Automation lock must not be a reparse point");
            }
        }
        Ok(fd_lock::RwLock::new(file))
    }

    fn with_transaction<T>(&self, operation: impl FnOnce() -> Result<T>) -> Result<T> {
        let mut lock = self.open_lock("state.lock")?;
        let _guard = lock.write().context("lock automation state")?;
        operation()
    }

View on GitHub (pinned to 433685b202)