Hmbown/CodeWhale · error · anyhow::Error

external agy credential store {} changed while being read

Error message

external agy credential store {} changed while being read

What it means

As a TOCTOU hardening step, the function pins the file identity (device+inode on Unix) of the secure-opened handle, runs the SQLite query (SQLite reopens the path by name), then reopens the path and compares identities. If the inode changed between the pin and the re-check, the bytes queried may not be the bytes validated, so the read is rejected rather than trusted.

Source

Thrown at crates/tui/src/agy_credentials.rs:123

        .metadata()
        .with_context(|| format!("statting {}", codewhale_config::quote_os_path(path)))?;
    if metadata.len() > AGY_STATE_DB_LIMIT {
        bail!(
            "external agy credential store {} exceeds the {} byte safety limit",
            codewhale_config::quote_os_path(path),
            AGY_STATE_DB_LIMIT
        );
    }
    // Pin the file identity: SQLite reopens the path by name, so hold the
    // secure handle open across the query and prove the inode did not move.
    let pinned = file_identity(&file);
    drop(file);
    let value = query_oauth_token(path)?;
    let reopened = std::fs::File::open(path)
        .ok()
        .and_then(|recheck| file_identity_of(&recheck));
    if pinned != reopened {
        bail!(
            "external agy credential store {} changed while being read",
            codewhale_config::quote_os_path(path)
        );
    }
    parse_agy_oauth_token_value(value)
}

#[cfg(unix)]
fn file_identity(file: &std::fs::File) -> Option<(u64, u64)> {
    use std::os::unix::fs::MetadataExt as _;
    file.metadata().ok().map(|m| (m.dev(), m.ino()))
}

#[cfg(unix)]
fn file_identity_of(file: &std::fs::File) -> Option<(u64, u64)> {
    file_identity(file)
}

View on GitHub (pinned to 0c42157ee5)

Solutions

  1. Close the Antigravity IDE/client so nothing rewrites state.vscdb, then retry the import.
  2. Retry with backoff in the caller — the replace is transient and the next attempt re-pins the new inode.
  3. Copy the store to a stable temp path (preserving the read-only grant semantics) and import from the copy if the source stays hot.

Example fix

// before
let token = antigravity_oauth_token_from_grant(&grant)?; // bails: changed while being read

// after
let mut attempt = 0;
let token = loop {
    match antigravity_oauth_token_from_grant(&grant) {
        Ok(t) => break t,
        Err(e) if e.to_string().contains("changed while being read") && attempt < 3 => {
            attempt += 1;
            std::thread::sleep(std::time::Duration::from_millis(200 * attempt));
        }
        Err(e) => return Err(e),
    }
};
Defensive patterns

Strategy: retry

Try / catch

let mut last = None;
for attempt in 0..3 {
    match antigravity_oauth_token_from_grant(&grant) {
        Ok(t) => break Ok(t),
        Err(e) if e.to_string().contains("changed while being read") => {
            last = Some(e);
            std::thread::sleep(std::time::Duration::from_millis(250 * (attempt + 1)));
        }
        Err(e) => break Err(e),
    }
}?;

Prevention

When it happens

Trigger: The agy client or IDE rewrites `state.vscdb` via rename/replace while antigravity_oauth_token_from_grant is mid-query — atomic-save patterns change the inode even though the path stays the same. Also a first-run migration or backup tool touching the profile directory during import.

Common situations: Importing credentials while the Antigravity IDE/CLI is still running and persists state; OS sync/indexing tools replacing files; a login flow writing a fresh token exactly during the read.

Related errors


AI-assisted analysis of Hmbown/CodeWhale@0c42157ee5 (2026-08-20). Data as JSON: /api/errors/e44aeac63e8f8906. Report an issue: GitHub.