{"record":{"id":"4af2cce45507b83e","repo":"Hmbown/CodeWhale","slug":"external-credential-path-must-name-a-non-reparse-regular","errorCode":null,"errorMessage":"external credential path must name a non-reparse regular file","messagePattern":"external credential path must name a non-reparse regular file","errorType":"validation","errorClass":"io::Error","httpStatus":null,"severity":"error","filePath":"crates/tui/src/external_credentials.rs","lineNumber":281,"sourceCode":"            io::ErrorKind::InvalidInput,\n            \"external credential path must be absolute and lexically normalized\",\n        ));\n    }\n\n    // Reject every reparse-point component before the final open. The final\n    // handle is opened as the reparse point itself, checked again, and its\n    // kernel-resolved path is compared below. A second component pass catches\n    // replacement during the open window.\n    reject_windows_reparse_components(path)?;\n    let file = std::fs::OpenOptions::new()\n        .read(true)\n        .custom_flags(FILE_FLAG_OPEN_REPARSE_POINT)\n        .open(path)?;\n    let metadata = file.metadata()?;\n    if metadata.file_attributes() & FILE_ATTRIBUTE_REPARSE_POINT != 0\n        || !metadata.file_type().is_file()\n    {\n        return Err(io::Error::new(\n            io::ErrorKind::InvalidInput,\n            \"external credential path must name a non-reparse regular file\",\n        ));\n    }\n    reject_windows_reparse_components(path)?;\n\n    let handle = file.as_raw_handle();\n    // Compare the spelling Windows actually opened rather than asking it to\n    // expand the path into its normalized long form. A valid caller path can\n    // contain an 8.3 component such as `RUNNER~1`; normalizing only the handle\n    // side would make that exact path look redirected. FILE_NAME_OPENED keeps\n    // the comparison handle-relative while the pre/post component checks above\n    // continue to reject reparse points and swaps.\n    let flags = FILE_NAME_OPENED | VOLUME_NAME_DOS;\n    // SAFETY: the handle remains owned by `file`; null output asks Windows for\n    // the required UTF-16 buffer length.\n    let needed = unsafe { GetFinalPathNameByHandleW(handle, std::ptr::null_mut(), 0, flags) };\n    if needed == 0 {","sourceCodeStart":263,"sourceCodeEnd":299,"githubUrl":"https://github.com/Hmbown/CodeWhale/blob/73e0f67d83c59909b571efdfc88c4bc28c309cb1/crates/tui/src/external_credentials.rs#L263-L299","documentation":"On Windows the library opens the path with FILE_FLAG_OPEN_REPARSE_POINT so it can inspect the raw target, and rejects it if the attribute is a reparse point (symlink, mount point, junction) or not a regular file. This throws InvalidInput to stop credentials being redirected through links/junctions to another location.","triggerScenarios":"On Windows, read_to_string or read_codewhale_owned_to_string is handed a path that is a symbolic link, directory junction, or mounted folder, or that resolves to a non-file object.","commonSituations":"User created a symlink from a synced/roaming folder to the real credential location; OneDrive/Dropbox placeholders or Dev Drive junctions sit in the path; a setup script used `mklink`/`New-Item -ItemType Junction` to relocate the credentials directory.","solutions":["Replace the symlink/junction with the real file: copy the target to the credential path and delete the link (`del <link>` / `Remove-Item`).","Move the credential file itself to the expected location rather than linking to it.","Check each path component (not just the final one) for junctions with `fsutil reparsepoint query` or `dir /AL` and eliminate them.","If a roaming-profile requirement forces redirection, change the configured credential path to the physical location."],"exampleFix":"// PowerShell, before\nNew-Item -ItemType SymbolicLink -Path $env:USERPROFILE\\.codewhale\\token.json -Target D:\\secrets\\token.json\n// after\nCopy-Item D:\\secrets\\token.json $env:USERPROFILE\\.codewhale\\token.json\nRemove-Item $env:USERPROFILE\\.codewhale\\token.json.link","handlingStrategy":"validation","validationCode":"// PowerShell pre-check\n// Get-Item $path | Select-Object LinkType, Attributes   # LinkType should be empty, no ReparsePoint attribute","typeGuard":"fn looks_like_reparse(path: &Path) -> bool {\n    // best-effort via std: symlink_metadata is the open-without-follow analog\n    std::fs::symlink_metadata(path)\n        .map(|md| md.file_type().is_symlink())\n        .unwrap_or(true)\n}","tryCatchPattern":"match read_codewhale_owned_to_string(&path) {\n    Ok(creds) => use(creds),\n    Err(e) if e.to_string().contains(\"non-reparse\") => {\n        eprintln!(\"{} is a symlink/junction; copy the real file instead of linking\", path.display());\n    }\n    Err(e) => return Err(e),\n}","preventionTips":["Store credentials as real files, never symlinks or junctions, on Windows.","Check every path component for junctions (`dir /AL` / `fsutil reparsepoint query`).","Be wary of OneDrive/Dropbox placeholder and Dev Drive redirection in the credential path.","If relocation is needed, change the configured path rather than creating a link."],"tags":["windows","security","symlink","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-23T08:17:48.524Z"}