{"record":{"id":"d773e15a3eb83a5e","repo":"Hmbown/CodeWhale","slug":"external-credential-consent-path-for-must-be-ab","errorCode":null,"errorMessage":"external credential consent path for {} must be absolute","messagePattern":"external credential consent path for (.+?) must be absolute","errorType":"validation","errorClass":"anyhow::Error","httpStatus":null,"severity":"error","filePath":"crates/config/src/external_credentials.rs","lineNumber":435,"sourceCode":"                self.consent_version\n            );\n        }\n        if self.provider != provider.as_str() {\n            bail!(\n                \"external credential consent is scoped to provider {:?}, not {}\",\n                self.provider,\n                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)","sourceCodeStart":417,"sourceCodeEnd":453,"githubUrl":"https://github.com/Hmbown/CodeWhale/blob/0c42157ee52f9d55af2b506d71b46249910f77d3/crates/config/src/external_credentials.rs#L417-L453","documentation":"validate_read_scope() requires the consented external-credential path to be absolute before normalization is even attempted; a relative path cannot define the exact file the read-only capability covers, so it is rejected outright.","triggerScenarios":"A consent record whose `path` field is relative (e.g. \"keys/acme.env\" or \"~/keys/acme.env\" before expansion) is validated for use; Path::is_absolute() is false and the bail fires.","commonSituations":"Users entering '~/' paths that were never expanded; relative paths working from one cwd in scripts but stored as-is; config copied between machines with home-relative shortcuts.","solutions":["Store the fully expanded absolute path in the consent (expand ~ yourself, e.g. /home/me/keys/acme.env)","Re-create the consent pointing at the absolute location of the credential file"],"exampleFix":"# before\npath = \"~/keys/acme.env\"\n\n# after\npath = \"/home/me/keys/acme.env\"","handlingStrategy":"validation","validationCode":"if !consent.path.is_absolute() {\n    let expanded = expand_tilde(&consent.path)?; // '~' -> $HOME, then check again\n    if !expanded.is_absolute() { return Err(anyhow!(\"consent path must be absolute\")); }\n}","typeGuard":"fn is_absolute_path(p: &Path) -> bool { p.is_absolute() }","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 absolute\") => {\n        reconsent_with_expanded_path(provider, source, &resolved).await\n    }\n    Err(e) => Err(e),\n}","preventionTips":["Expand '~' and relative paths before creating consents","Store paths as absolute in your provisioning scripts","Validate user-supplied credential paths at input time, not at read time"],"tags":["security","consent","path-validation","credentials"],"backgroundTag":"relative-path-rejected","analyzedSha":"0c42157ee52f9d55af2b506d71b46249910f77d3","analyzedAt":"2026-08-20T21:50:45.477Z","schemaVersion":2},"datasetVersion":"2026-08-21T18:17:14.833Z"}