Hmbown/CodeWhale · error · anyhow::Error
refusing to replace an existing xAI OAuth retirement path
Error message
refusing to replace an existing xAI OAuth retirement path
What it means
During a revocation on unix, each active credential is renamed to a tombstone `.xai-oauth-retired-<pid>-<nanos>-<counter>.tmp`; rename_raw fail-closed refuses to overwrite an existing destination. A collision requires the same pid, an identical nanosecond timestamp, and the same counter, so in practice this only happens after clock skew (VM snapshot restore, NTP step) combined with pid reuse.
Source
Thrown at crates/config/src/xai_credentials.rs:735
if unsafe { libc::unlinkat(self.directory_handle.as_raw_fd(), name.as_ptr(), 0) } != 0 {
let error = std::io::Error::last_os_error();
if error.kind() == std::io::ErrorKind::NotFound {
return Ok(false);
}
return Err(error).context("removing Codewhale-owned xAI OAuth file");
}
Ok(true)
}
fn rename_raw(&self, from: &str, to: &str) -> Result<()> {
use std::os::fd::AsRawFd as _;
validate_private_basename(from)?;
validate_private_basename(to)?;
let source = self
.open_owned_file_for_read(from)?
.context("xAI OAuth source disappeared before retirement")?;
validate_owned_file_handle(&source, &self.directory.join(from))?;
anyhow::ensure!(
self.open_owned_file_for_read(to)?.is_none(),
"refusing to replace an existing xAI OAuth retirement path"
);
drop(source);
let from = CString::new(from).context("xAI OAuth basename contains an interior NUL")?;
let to = CString::new(to).context("xAI OAuth basename contains an interior NUL")?;
// SAFETY: both names are one component relative to the same pinned
// directory descriptor.
if unsafe {
libc::renameat(
self.directory_handle.as_raw_fd(),
from.as_ptr(),
self.directory_handle.as_raw_fd(),
to.as_ptr(),
)
} != 0
{
return Err(std::io::Error::last_os_error()).context("retiring xAI OAuth file");View on GitHub (pinned to 8880682c63)
Solutions
- Retry the operation: the fresh timestamp/counter nonce makes a second attempt collide with near-zero probability
- When no codewhale process is running, delete stale .xai-oauth-retired-*.tmp files in $CODEWHALE_HOME/credentials
- If the clock was rewound (VM restore), let NTP stabilize time before retrying
Defensive patterns
Strategy: retry
Try / catch
let mut attempt = 0;
loop {
match codewhale_config::with_xai_oauth_revocation_transaction(&op) {
Ok(v) => break Ok(v),
Err(e) if e.to_string().contains("refusing to replace an existing xAI OAuth retirement path")
&& attempt < 2 =>
{
attempt += 1; // nonce collision: a retry gets a fresh timestamp
}
Err(e) => break Err(e),
}
} Prevention
- Keep system clocks stable (NTP) around revocation transactions, especially after VM restores
- Clean stale .xai-oauth-retired-*.tmp tombstones when no codewhale process runs
- Treat this error as transient in orchestrators; do not escalate on first occurrence
When it happens
Trigger: with_xai_oauth_revocation_transaction / stage_revocation where a stale tombstone from an earlier run received an identical generated name because SystemTime::now went backwards (VM restore, clock step) and the pid was reused.
Common situations: Laptops or VMs restored from snapshots with rewound clocks; CI runners with unstable clocks; otherwise effectively unreachable.
Related errors
- xAI OAuth credentials directory must be absolute
- Codewhale credentials path must be a directory
- Codewhale credentials directory must be owned by the current
- xAI OAuth path must be a regular file
- xAI OAuth file must not have multiple filesystem links
AI-assisted analysis of Hmbown/CodeWhale@8880682c63 (2026-08-16).
Data as JSON: /api/errors/bea83247d8394810.
Report an issue: GitHub.