Hmbown/CodeWhale · error · io::Error
external credential path must name a non-reparse regular…
Error message
external credential path must name a non-reparse regular file
What it means
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.
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.
Example fix
// PowerShell, before New-Item -ItemType SymbolicLink -Path $env:USERPROFILE\.codewhale\token.json -Target D:\secrets\token.json // after Copy-Item D:\secrets\token.json $env:USERPROFILE\.codewhale\token.json Remove-Item $env:USERPROFILE\.codewhale\token.json.link
Defensive patterns
Strategy: validation
Validate before calling
// PowerShell pre-check // Get-Item $path | Select-Object LinkType, Attributes # LinkType should be empty, no ReparsePoint attribute
Type guard
fn looks_like_reparse(path: &Path) -> bool {
// best-effort via std: symlink_metadata is the open-without-follow analog
std::fs::symlink_metadata(path)
.map(|md| md.file_type().is_symlink())
.unwrap_or(true)
} Try / catch
match read_codewhale_owned_to_string(&path) {
Ok(creds) => use(creds),
Err(e) if e.to_string().contains("non-reparse") => {
eprintln!("{} is a symlink/junction; copy the real file instead of linking", path.display());
}
Err(e) => return Err(e),
} Prevention
- 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.
When it happens
Trigger: 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.
Common situations: 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.
Understand the failure class
Background: Permission denied / not authorized / 403 Forbidden: access-control rejections when the caller lacks the required role, grant, or ownership — this error's family across 18 libraries.
Related errors
- Codewhale-owned credential file must be singly linked
- Codewhale-owned xAI OAuth path must not be a reparse point
- external credential path was redirected while opening
- reviewed plugin cwd is a reparse point or non-directory
- skill state lock at must be a regular, non-reparse file
AI-assisted analysis of Hmbown/CodeWhale@73e0f67d83 (2026-09-22).
Data as JSON: /api/errors/4af2cce45507b83e.
Report an issue: GitHub.
Appendix: source
Thrown at crates/tui/src/external_credentials.rs:281
io::ErrorKind::InvalidInput,
"external credential path must be absolute and lexically normalized",
));
}
// Reject every reparse-point component before the final open. The final
// handle is opened as the reparse point itself, checked again, and its
// kernel-resolved path is compared below. A second component pass catches
// replacement during the open window.
reject_windows_reparse_components(path)?;
let file = std::fs::OpenOptions::new()
.read(true)
.custom_flags(FILE_FLAG_OPEN_REPARSE_POINT)
.open(path)?;
let metadata = file.metadata()?;
if metadata.file_attributes() & FILE_ATTRIBUTE_REPARSE_POINT != 0
|| !metadata.file_type().is_file()
{
return Err(io::Error::new(
io::ErrorKind::InvalidInput,
"external credential path must name a non-reparse regular file",
));
}
reject_windows_reparse_components(path)?;
let handle = file.as_raw_handle();
// Compare the spelling Windows actually opened rather than asking it to
// expand the path into its normalized long form. A valid caller path can
// contain an 8.3 component such as `RUNNER~1`; normalizing only the handle
// side would make that exact path look redirected. FILE_NAME_OPENED keeps
// the comparison handle-relative while the pre/post component checks above
// continue to reject reparse points and swaps.
let flags = FILE_NAME_OPENED | VOLUME_NAME_DOS;
// SAFETY: the handle remains owned by `file`; null output asks Windows for
// the required UTF-16 buffer length.
let needed = unsafe { GetFinalPathNameByHandleW(handle, std::ptr::null_mut(), 0, flags) };
if needed == 0 {View on GitHub (pinned to 73e0f67d83)