{"record":{"id":"19a9dc98ada1a2df","repo":"Hmbown/CodeWhale","slug":"external-credential-consent-path-for-must-be-le","errorCode":null,"errorMessage":"external credential consent path for {} must be lexically normalized: {}","messagePattern":"external credential consent path for (.+?) must be lexically normalized: (.+?)","errorType":"validation","errorClass":"anyhow::Error","httpStatus":null,"severity":"error","filePath":"crates/config/src/external_credentials.rs","lineNumber":442,"sourceCode":"                provider.as_str()\n            );\n        }\n        if self.source != source {\n            bail!(\n                \"external credential consent source mismatch for {} (expected {})\",\n                provider.as_str(),\n                source.as_str()\n            );\n        }\n        if !self.path.is_absolute() {\n            bail!(\n                \"external credential consent path for {} must be absolute\",\n                provider.as_str()\n            );\n        }\n        let normalized = resolve_external_credential_path(&self.path)?;\n        if normalized != self.path {\n            bail!(\n                \"external credential consent path for {} must be lexically normalized: {}\",\n                provider.as_str(),\n                quote_os_path(&self.path)\n            );\n        }\n        if self.path != resolved_path {\n            bail!(\n                \"external credential path changed for {}; consent covers {}, current path is {}\",\n                provider.as_str(),\n                quote_os_path(&self.path),\n                quote_os_path(resolved_path)\n            );\n        }\n        Ok(())\n    }\n\n    /// Validate and mint the read capability consumed by credential adapters.\n    /// No filesystem operation occurs while validating the policy.","sourceCodeStart":424,"sourceCodeEnd":460,"githubUrl":"https://github.com/Hmbown/CodeWhale/blob/0c42157ee52f9d55af2b506d71b46249910f77d3/crates/config/src/external_credentials.rs#L424-L460","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","solutions":["Rewrite the consent path with all '.'/'..' components resolved: /keys/acme.env, not /keys/./acme.env","Re-create the consent from the real canonical location of the file"],"exampleFix":"# before\npath = \"/etc/codewhale/../codewhale/keys/acme.env\"\n\n# after\npath = \"/etc/codewhale/keys/acme.env\"","handlingStrategy":"validation","validationCode":"// Normalize at consent-creation time so stored == normalized:\nlet normalized = resolve_external_credential_path(&raw_path)?; // same lexical rules\n// store `normalized` in the consent record, never raw_path","typeGuard":"fn is_lexically_normalized(p: &Path) -> bool {\n    p.components().all(|c| !matches!(c, Component::CurDir | Component::ParentDir))\n        && resolve_external_credential_path(p).map(|n| n == p).unwrap_or(false)\n}","tryCatchPattern":"match consent.validate_read_scope(provider, source, &resolved) {\n    Ok(()) => read_external_credential(&resolved),\n    Err(e) if e.to_string().contains(\"must be lexically normalized\") => {\n        let clean = resolve_external_credential_path(&consent.path)?; // reconsent with `clean`\n        reconsent_with_path(provider, source, &clean).await\n    }\n    Err(e) => Err(e),\n}","preventionTips":["Create consents only through tooling that stores the normalized path","Avoid '.'/'..' segments in any stored credential path","After moving credential files, re-consent with the new direct path rather than writing traversal equivalents"],"tags":["security","consent","path-normalization","credentials"],"backgroundTag":"path-normalization-mismatch","analyzedSha":"0c42157ee52f9d55af2b506d71b46249910f77d3","analyzedAt":"2026-08-20T21:50:45.477Z","contentChangedAt":null,"schemaVersion":2},"datasetVersion":"2026-09-14T05:17:10.506Z"}