Hmbown/CodeWhale · warning

skill state lock at must be one regular, non-hard-linked…

Error message

skill state lock at {} must be one regular, non-hard-linked file

What it means

Before using a skill state lock file, validate_state_lock checks via metadata that the path is a regular file with exactly one hard link (nlink == 1). This guards against an attacker replacing the lock with a symlink target, directory, or hard-linked file that could be manipulated through another path. Failing that check raises this error.

Solutions

  1. Inspect the path (`ls -la`, `stat`) and replace it with a fresh regular file: delete it and let the app recreate the lock.
  2. Find and remove other hard links (`find / -samefile <lockfile>`) so nlink returns to 1.
  3. Ensure the skill state directory is private to the user (e.g. 0700) so others cannot plant links or symlinks.
  4. Verify no symlink exists at the path (`readlink`); remove it if present.

Example fix

# before: lock is a symlink planted in a shared dir
$ rm ~/.local/state/skills/<skill>/lock
$ chmod 700 ~/.local/state/skills
// after: app recreates a plain regular lock file
Defensive patterns

Strategy: try-catch

Validate before calling

let md = std::fs::symlink_metadata(&lock_path)?;
if !md.is_file() || md.file_type().is_symlink() { /* recreate lock: remove path first */ }
let nlink = std::fs::metadata(&lock_path)?.nlink();
if nlink != 1 { eprintln!("hard links present; remove duplicates before running"); }

Type guard

fn is_plain_regular_file(p: &Path) -> bool { std::fs::symlink_metadata(p).map(|m| m.is_file() && !m.file_type().is_symlink()).unwrap_or(false) }

Try / catch

match open_state_lock(path) {
    Err(e) if e.to_string().contains("must be one regular, non-hard-linked file") => {
        fs::remove_file(path).ok();
        open_state_lock(path) // recreate a clean lock
    }
    other => other,
}

Prevention

When it happens

Trigger: Opening a skill state lock when the lock path points at a directory, a symlink to elsewhere, a device/special file, or a file with nlink > 1 (hard-linked from another directory).

Common situations: Tampering or attacks on a shared multi-user lock directory; restore/backup tools that hard-link files into the state dir; someone symlinking the lock path; misconfigured skill state directory shared between users.

Understand the failure class

Background: "is not a compatible type" / "cannot merge" errors: when a value's type doesn't match what the library requires — this error's family across 65 libraries.

Related errors


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

Appendix: source

Thrown at crates/tui/src/skill_state.rs:212

    {
        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 skill state lock at {}", path.display()))?;
    validate_state_lock(path, &file)?;
    Ok(file)
}

#[cfg(unix)]
fn validate_state_lock(path: &Path, file: &fs::File) -> Result<()> {
    use std::os::unix::fs::MetadataExt as _;

    let metadata = file
        .metadata()
        .with_context(|| format!("inspect skill state lock at {}", path.display()))?;
    anyhow::ensure!(
        metadata.is_file() && metadata.nlink() == 1,
        "skill state lock at {} must be one regular, non-hard-linked file",
        path.display()
    );
    Ok(())
}

#[cfg(windows)]
fn validate_state_lock(path: &Path, file: &fs::File) -> Result<()> {
    use std::os::windows::fs::MetadataExt as _;

    const FILE_ATTRIBUTE_REPARSE_POINT: u32 = 0x0000_0400;
    let metadata = file
        .metadata()
        .with_context(|| format!("inspect skill state lock at {}", path.display()))?;
    anyhow::ensure!(
        metadata.is_file() && metadata.file_attributes() & FILE_ATTRIBUTE_REPARSE_POINT == 0,
        "skill state lock at {} must be a regular, non-reparse file",

View on GitHub (pinned to 433685b202)