jdx/mise · error
brew-cask: temporary artifact directory was replaced
Error message
brew-cask: temporary artifact directory was replaced
What it means
remove_private_staging_dir deletes the private staging directory used for an elevated/generic artifact operation. Before unlinkat it fstat()s the directory fd it holds and compares st_dev/st_ino against the inode currently linked under the staging name in its parent (fstatat with AT_SYMLINK_NOFOLLOW). A mismatch means the name now points at a different directory than the one mise created and verified, so mise refuses to unlink it — a TOCTOU defense against a swapped directory.
Solutions
- Re-run the mise command once no other mise/brew process is touching the same caskroom
- Serialize installs of the same cask (per-token lock) in automation instead of running them in parallel
- After aborted concurrent jobs, remove leftover staging directories under the caskroom/state tree and retry
- If it recurs with zero concurrency, inspect the parent dir for a name-swapping process and report it as a security issue
Example fix
# before: two racing jobs $ mise brew install --cask foo & mise brew install --cask foo & # after: serialized $ mise brew install --cask foo && mise brew install --cask foo
Defensive patterns
Strategy: retry
Validate before calling
// Serialize operations per cask token before installing.
let lock = std::fs::File::create(format!("/tmp/mise-cask-{}.lock", token))?;
use std::os::unix::fs::PermissionsExt;
lock.set_permissions(std::fs::Permissions::from_mode(0o644))?;
flock_exclusive(&lock)?; // hold until the operation finishes Try / catch
match install_cask(&cask) {
Err(e) if e.to_string().contains("temporary artifact directory was replaced") => {
// transient race: wait, verify no concurrent run, retry once
std::thread::sleep(std::time::Duration::from_secs(2));
install_cask(&cask)
}
other => other,
} Prevention
- Never run two mise/brew operations against the same caskroom concurrently
- Hold a per-token lock in automation harnesses
- Clean leftover staging directories after aborted jobs before retrying
When it happens
Trigger: Another process unlinks and recreates the staging directory name between its mkdirat/openat and the removal: a concurrent mise or Homebrew run on the same caskroom, a cleanup script, or (deliberately) a same-uid attacker racing the operation.
Common situations: Two parallel mise/brew install or uninstall jobs touching the same cask token; CI pipelines racing on a shared caskroom; aggressive tmp/caskroom cleaners running mid-install.
Related errors
- brew-cask: : Homebrew took ownership of this cask while…
- brew-cask: refusing operation through a changed generic…
- brew-cask: refusing operation through a changed generic…
- brew-cask: binary artifact
- brew-cask: binary artifact
AI-assisted analysis of jdx/mise@6f52dcdf99 (2026-08-22).
Data as JSON: /api/errors/d19801dff1899b4c.
Report an issue: GitHub.
Appendix: source
Thrown at src/system/packages/brew/cask.rs:2173
nix::unistd::unlinkat(parent, name, nix::unistd::UnlinkatFlags::NoRemoveDir)?;
}
Ok(())
}
#[cfg(unix)]
fn remove_private_staging_dir(
parent: &TrustedOperationParent,
staging: &TrustedOperationParent,
staging_name: &std::ffi::OsStr,
) -> Result<()> {
let bound = nix::sys::stat::fstat(&staging.fd)?;
let linked = nix::sys::stat::fstatat(
&parent.fd,
staging_name,
nix::fcntl::AtFlags::AT_SYMLINK_NOFOLLOW,
)?;
if bound.st_dev != linked.st_dev || bound.st_ino != linked.st_ino {
bail!("brew-cask: temporary artifact directory was replaced");
}
nix::unistd::unlinkat(
&parent.fd,
staging_name,
nix::unistd::UnlinkatFlags::RemoveDir,
)?;
Ok(())
}
fn validate_generic_copy_target(target: &Path) -> Result<()> {
let prefix = prefix::prefix();
if !target.starts_with(&prefix)
|| target.strip_prefix(&prefix)?.components().next().is_none()
|| !path_starts_with_resolved_root(target, &prefix)
{
bail!(
"brew-cask: refusing generic artifact copy outside Homebrew prefix: {}",
target.display()View on GitHub (pinned to 6f52dcdf99)