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

  1. Sign in with ChatGPT via `codewhale auth chatgpt` to use Codewhale's own credentials.
  2. Run `codex login` again to refresh the Codex CLI's stored token.
  3. Set OPENAI_CODEX_ACCESS_TOKEN in the environment for this process.
  4. 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

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


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)