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

  1. Replace the symlink/junction with the real file: copy the target to the credential path and delete the link (`del <link>` / `Remove-Item`).
  2. Move the credential file itself to the expected location rather than linking to it.
  3. Check each path component (not just the final one) for junctions with `fsutil reparsepoint query` or `dir /AL` and eliminate them.
  4. 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

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


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)