{"record":{"id":"e44aeac63e8f8906","repo":"Hmbown/CodeWhale","slug":"external-agy-credential-store-changed-while-bei","errorCode":null,"errorMessage":"external agy credential store {} changed while being read","messagePattern":"external agy credential store (.+?) changed while being read","errorType":"exception","errorClass":"anyhow::Error","httpStatus":null,"severity":"error","filePath":"crates/tui/src/agy_credentials.rs","lineNumber":123,"sourceCode":"        .metadata()\n        .with_context(|| format!(\"statting {}\", codewhale_config::quote_os_path(path)))?;\n    if metadata.len() > AGY_STATE_DB_LIMIT {\n        bail!(\n            \"external agy credential store {} exceeds the {} byte safety limit\",\n            codewhale_config::quote_os_path(path),\n            AGY_STATE_DB_LIMIT\n        );\n    }\n    // Pin the file identity: SQLite reopens the path by name, so hold the\n    // secure handle open across the query and prove the inode did not move.\n    let pinned = file_identity(&file);\n    drop(file);\n    let value = query_oauth_token(path)?;\n    let reopened = std::fs::File::open(path)\n        .ok()\n        .and_then(|recheck| file_identity_of(&recheck));\n    if pinned != reopened {\n        bail!(\n            \"external agy credential store {} changed while being read\",\n            codewhale_config::quote_os_path(path)\n        );\n    }\n    parse_agy_oauth_token_value(value)\n}\n\n#[cfg(unix)]\nfn file_identity(file: &std::fs::File) -> Option<(u64, u64)> {\n    use std::os::unix::fs::MetadataExt as _;\n    file.metadata().ok().map(|m| (m.dev(), m.ino()))\n}\n\n#[cfg(unix)]\nfn file_identity_of(file: &std::fs::File) -> Option<(u64, u64)> {\n    file_identity(file)\n}\n","sourceCodeStart":105,"sourceCodeEnd":141,"githubUrl":"https://github.com/Hmbown/CodeWhale/blob/0c42157ee52f9d55af2b506d71b46249910f77d3/crates/tui/src/agy_credentials.rs#L105-L141","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","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."],"exampleFix":"// before\nlet token = antigravity_oauth_token_from_grant(&grant)?; // bails: changed while being read\n\n// after\nlet mut attempt = 0;\nlet token = loop {\n    match antigravity_oauth_token_from_grant(&grant) {\n        Ok(t) => break t,\n        Err(e) if e.to_string().contains(\"changed while being read\") && attempt < 3 => {\n            attempt += 1;\n            std::thread::sleep(std::time::Duration::from_millis(200 * attempt));\n        }\n        Err(e) => return Err(e),\n    }\n};","handlingStrategy":"retry","validationCode":null,"typeGuard":null,"tryCatchPattern":"let mut last = None;\nfor attempt in 0..3 {\n    match antigravity_oauth_token_from_grant(&grant) {\n        Ok(t) => break Ok(t),\n        Err(e) if e.to_string().contains(\"changed while being read\") => {\n            last = Some(e);\n            std::thread::sleep(std::time::Duration::from_millis(250 * (attempt + 1)));\n        }\n        Err(e) => break Err(e),\n    }\n}?;","preventionTips":["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."],"tags":["antigravity","toctou","file-identity","credentials"],"backgroundTag":"file-changed-during-read","analyzedSha":"0c42157ee52f9d55af2b506d71b46249910f77d3","analyzedAt":"2026-08-20T21:50:45.477Z","contentChangedAt":null,"schemaVersion":2},"datasetVersion":"2026-09-14T00:17:10.932Z"}