{"record":{"id":"5bd45f6547e12b2c","repo":"Hmbown/CodeWhale","slug":"skill-state-lock-at-must-be-one-regular-non-hard-linked-file","errorCode":null,"errorMessage":"skill state lock at {} must be one regular, non-hard-linked file","messagePattern":"skill state lock at (.+?) must be one regular, non-hard-linked file","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"warning","filePath":"crates/tui/src/skill_state.rs","lineNumber":212,"sourceCode":"    {\n        use std::os::windows::fs::OpenOptionsExt as _;\n        options.custom_flags(0x0020_0000); // FILE_FLAG_OPEN_REPARSE_POINT\n    }\n    let file = options\n        .open(path)\n        .with_context(|| format!(\"open skill state lock at {}\", path.display()))?;\n    validate_state_lock(path, &file)?;\n    Ok(file)\n}\n\n#[cfg(unix)]\nfn validate_state_lock(path: &Path, file: &fs::File) -> Result<()> {\n    use std::os::unix::fs::MetadataExt as _;\n\n    let metadata = file\n        .metadata()\n        .with_context(|| format!(\"inspect skill state lock at {}\", path.display()))?;\n    anyhow::ensure!(\n        metadata.is_file() && metadata.nlink() == 1,\n        \"skill state lock at {} must be one regular, non-hard-linked file\",\n        path.display()\n    );\n    Ok(())\n}\n\n#[cfg(windows)]\nfn validate_state_lock(path: &Path, file: &fs::File) -> Result<()> {\n    use std::os::windows::fs::MetadataExt as _;\n\n    const FILE_ATTRIBUTE_REPARSE_POINT: u32 = 0x0000_0400;\n    let metadata = file\n        .metadata()\n        .with_context(|| format!(\"inspect skill state lock at {}\", path.display()))?;\n    anyhow::ensure!(\n        metadata.is_file() && metadata.file_attributes() & FILE_ATTRIBUTE_REPARSE_POINT == 0,\n        \"skill state lock at {} must be a regular, non-reparse file\",","sourceCodeStart":194,"sourceCodeEnd":230,"githubUrl":"https://github.com/Hmbown/CodeWhale/blob/433685b2024e7bc4c99e1e2e326bcad39b4d9d65/crates/tui/src/skill_state.rs#L194-L230","documentation":"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.","triggerScenarios":"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).","commonSituations":"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.","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."],"exampleFix":"# before: lock is a symlink planted in a shared dir\n$ rm ~/.local/state/skills/<skill>/lock\n$ chmod 700 ~/.local/state/skills\n// after: app recreates a plain regular lock file","handlingStrategy":"try-catch","validationCode":"let md = std::fs::symlink_metadata(&lock_path)?;\nif !md.is_file() || md.file_type().is_symlink() { /* recreate lock: remove path first */ }\nlet nlink = std::fs::metadata(&lock_path)?.nlink();\nif nlink != 1 { eprintln!(\"hard links present; remove duplicates before running\"); }","typeGuard":"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) }","tryCatchPattern":"match open_state_lock(path) {\n    Err(e) if e.to_string().contains(\"must be one regular, non-hard-linked file\") => {\n        fs::remove_file(path).ok();\n        open_state_lock(path) // recreate a clean lock\n    }\n    other => other,\n}","preventionTips":["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."],"tags":["security","filesystem","symlink"],"backgroundTag":"incompatible-source-type","analyzedSha":"433685b2024e7bc4c99e1e2e326bcad39b4d9d65","analyzedAt":"2026-09-15T12:24:24.634Z","contentChangedAt":"2026-09-15T12:24:24.634Z","schemaVersion":2},"datasetVersion":"2026-09-23T08:17:48.524Z"}