{"record":{"id":"442e6974f098b88f","repo":"Hmbown/CodeWhale","slug":"external-credential-path-changed-for-consent-c","errorCode":null,"errorMessage":"external credential path changed for {}; consent covers {}, current path is {}","messagePattern":"external credential path changed for (.+?); consent covers (.+?), current path is (.+?)","errorType":"validation","errorClass":"anyhow::Error","httpStatus":null,"severity":"error","filePath":"crates/config/src/external_credentials.rs","lineNumber":449,"sourceCode":"                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.\n    pub fn read_grant(\n        &self,\n        provider: ProviderKind,\n        source: ExternalCredentialSource,\n        resolved_path: &Path,\n    ) -> Result<ExternalCredentialReadGrant> {\n        self.validate_read_scope(provider, source, resolved_path)?;","sourceCodeStart":431,"sourceCodeEnd":467,"githubUrl":"https://github.com/Hmbown/CodeWhale/blob/0c42157ee52f9d55af2b506d71b46249910f77d3/crates/config/src/external_credentials.rs#L431-L467","documentation":"Thrown by ExternalCredentialConsentToml::validate_read_scope (crates/config/src/external_credentials.rs:448) when the persisted read-only consent record pins a different absolute path than the credential path the caller currently resolves. Consent is deliberately scoped to one exact provider/source/path tuple, so any drift fails closed instead of silently granting read access to a new file. The record itself is untouched; only the capability check fails.","triggerScenarios":"Calling validate_read_scope or read_grant with a resolved_path that differs from self.path (the absolute, lexically normalized consented path): the env var backing the credential source was repointed, the credential file was moved/renamed, HOME or the workspace changed the resolution, or the provider table was copied to another machine with different layout.","commonSituations":"Rotating service-account files (new filename, old consent), syncing dotfiles between machines, CI with a different HOME, re-arranging ~/.config or a credentials directory after running the consent flow once.","solutions":["Re-run the consent flow for the provider so the record stores the current resolved path (after it, validate covers the new file)","If the old file is still the intended one, point the source env var/config back to the consented path shown in the message (consent covers <old>, current path is <new>)","Run `codewhale auth external-revoke --provider <provider>` if you want to clear the stale consent before re-consenting","Check the consent status surface (ambient_path_changed field in ExternalCredentialConsentStatus) to confirm the drift before acting"],"exampleFix":"# before\nexport MY_PROVIDER_CREDENTIALS=/home/me/creds/old-key.json   # consent covers this\nmv /home/me/creds/old-key.json /home/me/creds/new-key.json\n# read_grant -> path changed for <provider>\n\n# after\nexport MY_PROVIDER_CREDENTIALS=/home/me/creds/new-key.json\ncodewhale auth external-revoke --provider <provider>\n# re-run the external-credential consent flow so consent covers new-key.json","handlingStrategy":"validation","validationCode":"// before read_grant, compare the consent record path with the currently resolved path\nlet consent: &ExternalCredentialConsentToml = /* loaded */;\nif consent.path != resolved_path {\n    // re-run the consent flow instead of calling read_grant\n}","typeGuard":null,"tryCatchPattern":"match consent.read_grant(provider, source, &resolved_path) {\n    Ok(grant) => { /* use grant */ }\n    Err(e) if e.to_string().contains(\"external credential path changed\") => { /* prompt re-consent */ }\n    Err(e) => return Err(e),\n}","preventionTips":["Store the consent path next to the credential and alert when the source env var is repointed","Surface the ambient_path_changed status flag in any UI that lists external credentials","Revoke and re-consent as part of credential rotation runbooks"],"tags":["rust","credentials","consent","security","path-mismatch"],"backgroundTag":"credential-consent-path-mismatch","analyzedSha":"0c42157ee52f9d55af2b506d71b46249910f77d3","analyzedAt":"2026-08-20T21:50:45.477Z","schemaVersion":2},"datasetVersion":"2026-08-21T23:17:16.201Z"}