Hmbown/CodeWhale · error · anyhow::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 file is regular, the manager checks metadata.nlink() == 1. Multiple hard links to the lock file break the assumption that holding the fd lock protects the unique lock resource (another process could swap paths), so any file with nlink != 1 is rejected.

Solutions

  1. Delete the extra hard link so the lock path has nlink == 1 (find links via find -samefile), then retry
  2. Remove the lock file entirely and let the manager recreate it
  3. Exclude the state/lock directory from hard-link-based backup/sync jobs

Example fix

// shell
// before
ln ~/.app/state/automation.lock ~/backup/automation.lock  # nlink=2, rejected
// after
ln -s ~/.app/state/automation.lock ~/backup/automation.lock  # symlink does not raise nlink
Defensive patterns

Strategy: validation

Validate before calling

#[cfg(unix)]
{
    use std::os::unix::fs::MetadataExt;
    let n = std::fs::metadata(&lock_path)?.nlink();
    assert_eq!(n, 1, "lock file must not be hard-linked");
}

Try / catch

if err.to_string().contains("must not have hard links") {
    std::fs::remove_file(&lock_path).ok(); // recreate as a fresh single-link file
}

Prevention

When it happens

Trigger: The lock file was hard-linked elsewhere (ln without -s), or a backup/sync tool created hard links at the lock path before the manager opened it.

Common situations: Backup tools (rsync --link-dest, git worktree-style dedup) hard-linking state directories; a user manually hard-linking to 'back up' the lock; scripts that link state files into shared caches.

Understand the failure class

Background: "Must be a positive integer", "Invalid value", "Unsupported": the invalid-argument-value error family, when a library rejects the value you pass — this error's family across 35 libraries.

Related errors


AI-assisted analysis of Hmbown/CodeWhale@73e0f67d83 (2026-09-22). 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 73e0f67d83)