{"record":{"id":"f9657533a24e6729","repo":"Hmbown/CodeWhale","slug":"provider-catalog-lock-must-not-be-hard-linked","errorCode":null,"errorMessage":"provider catalog lock {} must not be hard linked","messagePattern":"provider catalog lock (.+?) must not be hard linked","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"error","filePath":"crates/tui/src/provider_catalog_live.rs","lineNumber":545,"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 provider catalog lock {}\", path.display()))?;\n    let metadata = file\n        .metadata()\n        .with_context(|| format!(\"inspect provider catalog lock {}\", path.display()))?;\n    anyhow::ensure!(\n        metadata.is_file(),\n        \"provider catalog lock {} must be a regular file\",\n        path.display()\n    );\n    #[cfg(unix)]\n    {\n        use std::os::unix::fs::MetadataExt as _;\n        anyhow::ensure!(\n            metadata.nlink() == 1,\n            \"provider catalog lock {} must not be hard linked\",\n            path.display()\n        );\n    }\n    #[cfg(windows)]\n    {\n        use std::os::windows::fs::MetadataExt as _;\n        const FILE_ATTRIBUTE_REPARSE_POINT: u32 = 0x0000_0400;\n        anyhow::ensure!(\n            metadata.file_attributes() & FILE_ATTRIBUTE_REPARSE_POINT == 0,\n            \"provider catalog lock {} must not be a reparse point\",\n            path.display()\n        );\n    }\n    Ok(file)\n}\n","sourceCodeStart":527,"sourceCodeEnd":563,"githubUrl":"https://github.com/Hmbown/CodeWhale/blob/73e0f67d83c59909b571efdfc88c4bc28c309cb1/crates/tui/src/provider_catalog_live.rs#L527-L563","documentation":"On Unix, the provider catalog lock file must have an nlink count of 1. If it is hard linked elsewhere, another process could swap the underlying inode and bypass the advisory lock, so locking is refused as a security/integrity guard.","triggerScenarios":"`open_cache_lock` (called by `load_from_disk`, `persist_scope`, `persist_failure_scope`) on Unix sees `metadata.nlink() != 1` — the lock file has additional hard links.","commonSituations":"Backup/dedup tools (rsync --link-dest, git-annex, content-addressed stores) hard-linked the cache file, a user hard-linked the lock across directories, or a snapshot/restore tool relinked cache files.","solutions":["Delete the lock file (`rm <path>`) and retry; it is recreated with a single link.","Find the other links with `find / -samefile <path> 2>/dev/null` and remove them or exclude the cache dir from dedup tools.","Reconfigure backup/dedup tooling to skip the codewhale cache directory.","Restore the cache directory from a clean state if links keep reappearing."],"exampleFix":"// before: lock has 2 links\n$ stat -c %h ~/.cache/codewhale/provider-catalog.lock\n2\n// after\n$ rm ~/.cache/codewhale/provider-catalog.lock\n$ stat -c %h ~/.cache/codewhale/provider-catalog.lock  # recreated as 1","handlingStrategy":"validation","validationCode":"import { statSync } from 'node:fs';\nif (process.platform !== 'win32') {\n  const st = statSync(lockPath);\n  if (st.nlink > 1) {\n    console.error(`${lockPath} is hard linked ${st.nlink}x — remove before running`);\n    process.exit(1);\n  }\n}","typeGuard":"const hasSingleLink = (p) => { try { return statSync(p).nlink === 1; } catch { return false; } };","tryCatchPattern":"try {\n  await codewhale.providers.refresh();\n} catch (e) {\n  if (e.message.includes('must not be hard linked')) {\n    fs.rmSync(lockPath, { force: true });\n    return codewhale.providers.refresh();\n  }\n  throw e;\n}","preventionTips":["Exclude the codewhale cache directory from dedup/hard-link tools (rsync --link-dest, git-annex).","Never hard-link files out of the cache directory manually.","If links reappear after backups, restore the cache from a fresh run instead."],"tags":["file-locking","security","unix","rust"],"backgroundTag":"file-open-failed","analyzedSha":"73e0f67d83c59909b571efdfc88c4bc28c309cb1","analyzedAt":"2026-09-22T01:30:00.501Z","contentChangedAt":"2026-09-22T01:30:00.501Z","schemaVersion":2},"datasetVersion":"2026-09-23T08:17:48.524Z"}