Hmbown/CodeWhale · error · anyhow::Error

external credential consent path for {} must be lexically no

Error message

external credential consent path for {} must be lexically normalized: {}

What it means

The consent must name its path in already-normalized lexical form: validate_read_scope() re-runs resolve_external_credential_path() on the stored path and requires the result to equal the stored value byte-for-byte. Any '.', '..', or redundant component that normalization would strip makes them differ and the consent is refused — forcing the stored grant to match exactly what will be opened.

Source

Thrown at crates/config/src/external_credentials.rs:442

                provider.as_str()
            );
        }
        if self.source != source {
            bail!(
                "external credential consent source mismatch for {} (expected {})",
                provider.as_str(),
                source.as_str()
            );
        }
        if !self.path.is_absolute() {
            bail!(
                "external credential consent path for {} must be absolute",
                provider.as_str()
            );
        }
        let normalized = resolve_external_credential_path(&self.path)?;
        if normalized != self.path {
            bail!(
                "external credential consent path for {} must be lexically normalized: {}",
                provider.as_str(),
                quote_os_path(&self.path)
            );
        }
        if self.path != resolved_path {
            bail!(
                "external credential path changed for {}; consent covers {}, current path is {}",
                provider.as_str(),
                quote_os_path(&self.path),
                quote_os_path(resolved_path)
            );
        }
        Ok(())
    }

    /// Validate and mint the read capability consumed by credential adapters.
    /// No filesystem operation occurs while validating the policy.

View on GitHub (pinned to 0c42157ee5)

Solutions

  1. Rewrite the consent path with all '.'/'..' components resolved: /keys/acme.env, not /keys/./acme.env
  2. Re-create the consent from the real canonical location of the file

Example fix

# before
path = "/etc/codewhale/../codewhale/keys/acme.env"

# after
path = "/etc/codewhale/keys/acme.env"
Defensive patterns

Strategy: validation

Validate before calling

// Normalize at consent-creation time so stored == normalized:
let normalized = resolve_external_credential_path(&raw_path)?; // same lexical rules
// store `normalized` in the consent record, never raw_path

Type guard

fn is_lexically_normalized(p: &Path) -> bool {
    p.components().all(|c| !matches!(c, Component::CurDir | Component::ParentDir))
        && resolve_external_credential_path(p).map(|n| n == p).unwrap_or(false)
}

Try / catch

match consent.validate_read_scope(provider, source, &resolved) {
    Ok(()) => read_external_credential(&resolved),
    Err(e) if e.to_string().contains("must be lexically normalized") => {
        let clean = resolve_external_credential_path(&consent.path)?; // reconsent with `clean`
        reconsent_with_path(provider, source, &clean).await
    }
    Err(e) => Err(e),
}

Prevention

When it happens

Trigger: A consent path like "/keys/../keys/acme.env" or "/keys/./acme.env" (or with a trailing artifact) normalizes to a different value than stored, so normalized != self.path trips the bail.

Common situations: Hand-edited or programmatically joined consent paths containing '.'/'..' segments; paths pasted from shells with redundant components; symlinks in the directory making users write traversal-style equivalents.

Related errors


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