Hmbown/CodeWhale · error
Codex access token in
Error message
Codex access token in {} is expired. Read-only consent never refreshes or rewrites another CLI's credentials. Sign in with ChatGPT via `codewhale auth chatgpt`, run `codex login` again, or provide OPENAI_CODEX_ACCESS_TOKEN for this process. What it means
The Codex OAuth access token stored by the external `codex` CLI is expired. Codewhale only holds read-only consent to that credential file and deliberately never refreshes or rewrites another CLI's tokens, so instead of silently refreshing it fails and tells you how to obtain a fresh token. The error names the credential file path so you know exactly which grant is stale.
Solutions
- Sign in with ChatGPT via `codewhale auth chatgpt` to use Codewhale's own credentials.
- Run `codex login` again to refresh the Codex CLI's stored token.
- Set OPENAI_CODEX_ACCESS_TOKEN in the environment for this process.
- Check the token file's modified time and expiry to confirm which credential is stale.
Example fix
// before let creds = get_credentials()?; // Codex access token ... is expired // after $ export OPENAI_CODEX_ACCESS_TOKEN=<fresh-token> let creds = get_credentials()?;
Defensive patterns
Strategy: try-catch
Validate before calling
// preflight: decode the JWT exp claim yourself before relying on the grant
fn token_expired(jwt: &str) -> bool {
let claims: serde_json::Value = decode_payload_unverified(jwt);
claims["exp"].as_i64().map(|e| e < now_unix()).unwrap_or(true)
} Try / catch
match get_credentials() {
Ok(creds) => creds,
Err(e) if e.to_string().contains("is expired") => {
// prompt: run `codex login` or `codewhale auth chatgpt`, or set OPENAI_CODEX_ACCESS_TOKEN
refresh_out_of_band()?;
get_credentials().context("token still expired after re-auth")?
}
Err(e) => return Err(e),
} Prevention
- Check token expiry before starting long sessions and re-auth proactively.
- Prefer `codewhale auth chatgpt` for flows where Codewhale owns the credential lifecycle.
- Set OPENAI_CODEX_ACCESS_TOKEN in automation environments where interactive login is impossible.
- Remember Codewhale never rewrites another CLI's credential file; schedule external `codex login` refreshes in cron/CI.
When it happens
Trigger: Calling get_credentials when token_is_expired(&creds.access_token) is true for the Codex grant, i.e. no valid token exists and none will be refreshed on your behalf.
Common situations: Using Codex sign-in after it has been idle past the access-token lifetime; a long-running session outliving the token; switching machines where the cached Codex token is stale; OPENAI_CODEX_ACCESS_TOKEN unset in the current process.
Related errors
- agy OAuth token member
- agy OAuth token JSON carries no access token member
- atomically replacing xAI OAuth credentials
- bearer credentials are not an API key
- cloud dispatch fails closed: the cloud agent runs Codewhale…
AI-assisted analysis of Hmbown/CodeWhale@73e0f67d83 (2026-09-22).
Data as JSON: /api/errors/1648becbb967d5fd.
Report an issue: GitHub.
Appendix: source
Thrown at crates/tui/src/oauth.rs:182
pub fn stored_credentials_present(grant: &ExternalCredentialReadGrant) -> bool {
load_credentials(grant)
.ok()
.flatten()
.is_some_and(|credentials| !token_is_expired(&credentials.access_token))
}
/// Load read-only credentials from the exact external path authorized by
/// `grant`. Expired tokens fail with guidance; they are never refreshed.
pub fn get_credentials(grant: &ExternalCredentialReadGrant) -> Result<CodexCredentials> {
let creds =
load_credentials(grant)?.with_context(|| missing_auth_message(OAuthProvider::Chatgpt))?;
// Check if the access token is still valid.
if !token_is_expired(&creds.access_token) {
return Ok(creds);
}
bail!(
"Codex access token in {} is expired. Read-only consent never refreshes or rewrites another CLI's credentials. Sign in with ChatGPT via `codewhale auth chatgpt`, run `codex login` again, or provide OPENAI_CODEX_ACCESS_TOKEN for this process.",
codewhale_config::quote_os_path(grant.path())
)
}
/// Read a ChatGPT account id from env overrides only.
fn codex_account_id_env() -> Option<String> {
for var in ["OPENAI_CODEX_ACCOUNT_ID", "CODEX_ACCOUNT_ID"] {
if let Ok(value) = std::env::var(var) {
let trimmed = value.trim();
if !trimmed.is_empty() {
return Some(trimmed.to_string());
}
}
}
None
}
View on GitHub (pinned to 73e0f67d83)