{"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 (InvalidInput)","httpStatus":null,"severity":"error","filePath":"crates/tui/src/external_credentials.rs","lineNumber":280,"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":262,"sourceCodeEnd":298,"githubUrl":"https://github.com/Hmbown/CodeWhale/blob/433685b2024e7bc4c99e1e2e326bcad39b4d9d65/crates/tui/src/external_credentials.rs#L262-L298","documentation":"On Windows, the file is opened with `FILE_FLAG_OPEN_REPARSE_POINT` so symlinks/junctions are not followed, and the resulting metadata must be a plain regular file with the `FILE_ATTRIBUTE_REPARSE_POINT` bit clear. Anything that is itself a reparse point (symlink, mount point, OneDrive placeholder, AppExecLink) is rejected with `InvalidInput`, because a reparse point could redirect the consented read to a different secret.","triggerScenarios":"Granting a Windows symlink or junction as the credential path; the credential file lives under a OneDrive-synced folder (cloud placeholder attributes) or is an AppExecLink; the user aliased the real file with `mklink`.","commonSituations":"Users storing dotfiles/config under OneDrive/Dropbox folders where placeholders are reparse points; developers symlinking `~/.codewhale` into another location; CI runners with redirected user profiles.","solutions":["Grant the real, physical file location (the junction/symlink target) instead of the link itself.","Move the credential file out of OneDrive/placeholder-backed folders into a local path, or mark the file as always-available offline so it dehydrates to a regular file.","Replace the symlink with a hard copy of the file (`Copy-Item`) rather than `New-Item -ItemType SymbolicLink`."],"exampleFix":"# before\nPS> New-Item -ItemType SymbolicLink -Path $env:USERPROFILE\\.codewhale\\claude.json -Target D:\\secrets\\claude.json\n# after\nPS> Copy-Item D:\\secrets\\claude.json $env:USERPROFILE\\.codewhale\\credentials\\claude.json","handlingStrategy":"validation","validationCode":"fn ensure_no_reparse(p: &std::path::Path) -> bool {\n    use std::os::windows::fs::MetadataExt;\n    match std::fs::metadata(p) {\n        Ok(md) => md.file_attributes() & 0x400 == 0, // FILE_ATTRIBUTE_REPARSE_POINT\n        Err(_) => false,\n    }\n}","typeGuard":null,"tryCatchPattern":"match read_to_string(&grant) {\n    Err(e) if e.to_string().contains(\"non-reparse\") => eprintln!(\"credential path is a symlink/junction/placeholder; grant the physical file instead\"),\n    other => other.map(|_| ()).map_err(Into::into),\n}","preventionTips":["Do not symlink or junction the credentials directory/file; copy instead.","Keep credentials out of OneDrive/Dropbox placeholder-backed folders.","Copy the real target file into the expected path rather than linking it."],"tags":["windows","symlink","security","filesystem"],"backgroundTag":"path-traversal-blocked","analyzedSha":"433685b2024e7bc4c99e1e2e326bcad39b4d9d65","analyzedAt":"2026-09-15T12:24:24.634Z","contentChangedAt":"2026-09-15T12:24:24.634Z","schemaVersion":2},"datasetVersion":"2026-09-22T01:17:13.364Z"}