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
- Close the Antigravity IDE/client so nothing rewrites state.vscdb, then retry the import.
- Retry with backoff in the caller — the replace is transient and the next attempt re-pins the new inode.
- 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
- Import credentials while the Antigravity client is closed so nothing replaces state.vscdb mid-read.
- Retry with short backoff — inode replacement is transient by nature.
- If the source stays hot, copy the db once (read-only) and read the copy.
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
- Antigravity import requires an agy_cli grant, not {}
- external agy credential file {} is not a SQLite database
- external agy credential store {} exceeds the {} byte safety
- agy OAuth token member `{member}` is empty
- agy OAuth token JSON carries no access token member
AI-assisted analysis of Hmbown/CodeWhale@0c42157ee5 (2026-08-20).
Data as JSON: /api/errors/e44aeac63e8f8906.
Report an issue: GitHub.