{"record":{"id":"1dea00f9cab01be9","repo":"Hmbown/CodeWhale","slug":"codewhale-owned-credential-file-must-be-singly-linked","errorCode":null,"errorMessage":"Codewhale-owned credential file must be singly linked","messagePattern":"Codewhale-owned credential file must be singly linked","errorType":"validation","errorClass":"io::Error","httpStatus":null,"severity":"error","filePath":"crates/tui/src/external_credentials.rs","lineNumber":331,"sourceCode":"    let expected = normalize_windows_path_for_comparison(path)?;\n    if actual != expected {\n        return Err(io::Error::new(\n            io::ErrorKind::PermissionDenied,\n            \"external credential path was redirected while opening\",\n        ));\n    }\n    if require_owner_only {\n        use windows_sys::Win32::Storage::FileSystem::{\n            BY_HANDLE_FILE_INFORMATION, GetFileInformationByHandle,\n        };\n        let mut information = BY_HANDLE_FILE_INFORMATION::default();\n        // SAFETY: the opened credential handle and output pointer remain valid\n        // for the duration of the call.\n        if unsafe { GetFileInformationByHandle(handle, &mut information) } == 0 {\n            return Err(io::Error::last_os_error());\n        }\n        if information.nNumberOfLinks != 1 {\n            return Err(io::Error::new(\n                io::ErrorKind::PermissionDenied,\n                \"Codewhale-owned credential file must be singly linked\",\n            ));\n        }\n        verify_windows_owner_only_handle(handle)?;\n    }\n    Ok(file)\n}\n\n/// Normalize a Windows path without replacement characters. Unpaired UTF-16\n/// is rejected so two distinct paths can never compare equal after a lossy\n/// conversion. This is intentionally stricter than filesystem display.\n#[cfg(windows)]\nfn normalize_windows_path_for_comparison(path: &Path) -> io::Result<String> {\n    let text = path.to_str().ok_or_else(|| {\n        io::Error::new(\n            io::ErrorKind::PermissionDenied,\n            \"credential path contains invalid Unicode and cannot be compared safely\",","sourceCodeStart":313,"sourceCodeEnd":349,"githubUrl":"https://github.com/Hmbown/CodeWhale/blob/73e0f67d83c59909b571efdfc88c4bc28c309cb1/crates/tui/src/external_credentials.rs#L313-L349","documentation":"For Codewhale-owned credentials on Windows, the library queries GetFileInformationByHandle and requires nNumberOfLinks == 1, i.e. the file must have no other hard links. It throws PermissionDenied otherwise, because a second hard link lets another local process hold an alternate name for the secret that survives permission checks on the primary path.","triggerScenarios":"read_codewhale_owned_to_string opens a Windows file whose link count is greater than 1, typically because `mklink /H` or `fsutil hardlink create` made an alias, or a copy tool created hard links.","commonSituations":"Backup/snapshot tools (dedupe, Storage Spaces, third-party backup) hard-link the credential file; a user created a hard link to share the token with another app; a container bind of the same underlying file shows extra links.","solutions":["Delete the extra hard links (`fsutil hardlink list <file>` to find them), leaving exactly one name.","Recreate the credential as a fresh single file: write a new file with restrictive ACLs and update the configuration.","Exclude the credential directory from deduplicating backup tools.","Never share the credential by hard link; grant access via ACLs instead."],"exampleFix":"// cmd, before\nmklink /H C:\\tools\\token.json C:\\Users\\me\\.codewhale\\token.json\n// after\ndel C:\\tools\\token.json\nfsutil hardlink list C:\\Users\\me\\.codewhale\\token.json   // should show a single link","handlingStrategy":"validation","validationCode":"// cmd pre-check: fsutil hardlink list <file>   — must print exactly one path","typeGuard":null,"tryCatchPattern":"match read_codewhale_owned_to_string(&path) {\n    Ok(creds) => use(creds),\n    Err(e) if e.to_string().contains(\"singly linked\") => {\n        eprintln!(\"remove extra hard links: fsutil hardlink list {}\", path.display());\n    }\n    Err(e) => return Err(e),\n}","preventionTips":["Never create hard links (mklink /H) to credential files.","Exclude credential directories from deduplicating backup software.","If sharing a secret with another app is needed, use ACL grants, not links.","Recreate the credential file fresh if link count is ever unclear."],"tags":["windows","security","hardlink","filesystem","credentials"],"backgroundTag":"permission-denied","analyzedSha":"73e0f67d83c59909b571efdfc88c4bc28c309cb1","analyzedAt":"2026-09-22T01:30:00.501Z","contentChangedAt":"2026-09-22T01:30:00.501Z","schemaVersion":2},"datasetVersion":"2026-09-22T06:17:15.046Z"}