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
- Inspect the path (`ls -la`, `stat`) and replace it with a fresh regular file: delete it and let the app recreate the lock.
- Find and remove other hard links (`find / -samefile <lockfile>`) so nlink returns to 1.
- Ensure the skill state directory is private to the user (e.g. 0700) so others cannot plant links or symlinks.
- 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
- Keep the skill state directory user-private (chmod 700).
- Never hard-link or symlink files into the state directory.
- After restoring from backups, recreate state files rather than copying links.
- Run as a single user per state directory; avoid shared group-writable locks.
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
- built-in plugin path may not be a symbolic link or reparse…
- CodewhalePet/1
- could not securely open
- external credential path must name a non-reparse regular…
- file is not a regular single-link file
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)