Hmbown/CodeWhale · error · anyhow::Error

Codewhale credentials directory must be lexically normalized

Error message

Codewhale credentials directory must be lexically normalized: {}

What it means

Thrown by lexical_absolute_path when the Codewhale credentials directory (built from codewhale_home(), i.e. $CODEWHALE_HOME or the default ~/.codewhale, joined with 'credentials') contains a '.' or '..' component after being made absolute. Canonicalization is deliberately never used here (the comment forbids following the filesystem), so the path must already be lexically normalized.

Source

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

}

/// 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))
}

/// Serialize every Codewhale-owned xAI OAuth lifecycle mutation across threads
/// and processes while pinning the lexical credentials directory.
///

View on GitHub (pinned to 0c42157ee5)

Solutions

  1. Set CODEWHALE_HOME to an absolute, normalized path with no '.' or '..' segments
  2. Build it with realpath in shell: export CODEWHALE_HOME="$(realpath "$BASE/state")"
  3. Unset CODEWHALE_HOME to fall back to the default ~/.codewhale location

Example fix

# before
export CODEWHALE_HOME="$BASE/../state"

# after
export CODEWHALE_HOME="$(realpath "$BASE/../state")"
Defensive patterns

Strategy: validation

Validate before calling

use std::path::{Component, PathBuf};

fn lexically_normal(p: &PathBuf) -> bool {
    !p.components().any(|c| matches!(c, Component::CurDir | Component::ParentDir))
}

if let Ok(home) = std::env::var("CODEWHALE_HOME") {
    anyhow::ensure!(lexically_normal(&PathBuf::from(&home)), "CODEWHALE_HOME must be absolute and normalized");
}

Type guard

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

Prevention

When it happens

Trigger: CODEWHALE_HOME set to a path like /home/u/./state, /home/u/foo/../x, or a relative value such as ../state that gets joined onto the cwd; the components check then finds CurDir/ParentDir and bails.

Common situations: Isolating state per project by exporting CODEWHALE_HOME with a hand-built path; CI images reusing a generic HOME with trailing '/./'; scripts that concatenate path fragments without normalizing.

Related errors


AI-assisted analysis of Hmbown/CodeWhale@0c42157ee5 (2026-08-20). Data as JSON: /api/errors/38844e5136356d7e. Report an issue: GitHub.