Hmbown/CodeWhale · error

Codewhale credentials directory must be lexically normalized

Error message

Codewhale credentials directory must be lexically normalized: {}

What it means

lexical_absolute_path normalizes the configured Codewhale (xAI) credentials directory to an absolute path and refuses paths that still contain `.` or `..` components, since lexically-normalized paths are required to pin the directory's identity safely. The library throws this to prevent aliasing or symlink-swap attacks that unnormalized relative segments would allow.

Solutions

  1. Normalize the path with std::fs::canonicalize (or clean it lexically with a path-normalization helper) before passing it as the credentials directory
  2. Remove `.`/`..` segments from the config value or env var that supplies the path
  3. Verify no symlinked or relative intermediate components remain after the change

Example fix

// before
let dir = format!("{}/.codewhale/../codewhale-creds", home);
// after
let dir = std::fs::canonicalize(format!("{}/codewhale-creds", home))?;
Defensive patterns

Strategy: validation

Validate before calling

use std::path::{Path, Component};
fn is_lexically_normalized(p: &Path) -> bool {
    !p.components().any(|c| matches!(c, Component::CurDir | Component::ParentDir))
}
assert!(is_lexically_normalized(&creds_dir), "normalize the credentials dir before use");

Type guard

fn is_lexically_normalized(p: &std::path::Path) -> bool {
    !p.components().any(|c| matches!(c, std::path::Component::CurDir | std::path::Component::ParentDir))
}

Try / catch

match xai_oauth_credentials_dir() {
    Ok(dir) => { /* use dir */ }
    Err(e) if e.to_string().contains("lexically normalized") => {
        let cleaned = std::fs::canonicalize(raw_path)?;
        retry_with(cleaned);
    }
    Err(e) => return Err(e),
}

Prevention

When it happens

Trigger: Calling xai_oauth_credentials_dir (or config loading that derives the credentials directory) with a path containing `.` or `..` components after joining, e.g. `/home/u/.codewhale/../.codewhale` or `./codewhale-creds`.

Common situations: Users building the credentials path by string concatenation or from env vars like HOME/XDG_CONFIG_HOME containing trailing relative segments; scripts constructing paths with `..`; config values copied from other tools that allow relative paths.

Understand the failure class

Background: "Invalid value" and "allowed values are" config errors: what your library rejected and how to fix it — this error's family across 41 libraries.

Related errors


AI-assisted analysis of Hmbown/CodeWhale@73e0f67d83 (2026-09-22). Data as JSON: /api/errors/38844e5136356d7e. Report an issue: GitHub.

Appendix: source

Thrown at crates/config/src/xai_credentials.rs:131

}

/// Make an owned path absolute without resolving any filesystem component.
/// Canonicalization is deliberately forbidden here: following an existing
/// `credentials` symlink would erase the lexical Codewhale-owned boundary and
/// turn an external directory into an apparently valid destination.
fn lexical_absolute_path(path: &Path) -> Result<PathBuf> {
    let absolute = if path.is_absolute() {
        path.to_path_buf()
    } else {
        std::env::current_dir()
            .context("resolving the Codewhale credentials directory")?
            .join(path)
    };
    if absolute
        .components()
        .any(|component| matches!(component, Component::CurDir | Component::ParentDir))
    {
        bail!(
            "Codewhale credentials directory must be lexically normalized: {}",
            crate::quote_os_path(&absolute)
        );
    }
    Ok(absolute)
}

pub fn xai_oauth_generation_path(generation: &str) -> Result<PathBuf> {
    Ok(xai_oauth_credentials_dir()?.join(validate_xai_oauth_generation(generation)?))
}

pub fn legacy_xai_oauth_path() -> Result<PathBuf> {
    Ok(xai_oauth_credentials_dir()?.join(LEGACY_XAI_OAUTH_FILE_NAME))
}

pub fn chatgpt_oauth_generation_path(generation: &str) -> Result<PathBuf> {
    Ok(xai_oauth_credentials_dir()?.join(validate_chatgpt_oauth_generation(generation)?))
}

View on GitHub (pinned to 73e0f67d83)