jdx/mise · critical
brew-cask: temporary artifact directory is not private
Error message
brew-cask: temporary artifact directory is not private
What it means
For atomic installs mise opens a private staging directory inside a trusted parent (openat with O_NOFOLLOW) and then fstats it: the directory must be owned by the current euid and have no group/other permission bits (mode & 0o077 == 0). If ownership or permissions are looser — a pre-planted or leftover directory — the install aborts, because anything readable or writable by others could be tampered with between staging and the rename.
Source
Thrown at src/system/packages/brew/cask.rs:1779
.ok_or_else(|| eyre!("brew-cask: generic artifact target has no filename"))?;
let staging_name = format!(".mise-copy-{}", crate::rand::random_string(16));
nix::sys::stat::mkdirat(
&parent.fd,
staging_name.as_str(),
nix::sys::stat::Mode::S_IRWXU,
)?;
let flags = nix::fcntl::OFlag::O_RDONLY
| nix::fcntl::OFlag::O_DIRECTORY
| nix::fcntl::OFlag::O_NOFOLLOW;
let staging_fd = nix::fcntl::openat(
&parent.fd,
staging_name.as_str(),
flags,
nix::sys::stat::Mode::empty(),
)?;
let staging_stat = nix::sys::stat::fstat(&staging_fd)?;
if staging_stat.st_uid != nix::unistd::geteuid().as_raw() || staging_stat.st_mode & 0o077 != 0 {
bail!("brew-cask: temporary artifact directory is not private");
}
let staging = TrustedOperationParent { fd: staging_fd };
let temporary_name = std::ffi::OsStr::new("payload");
match copy_cask_artifact_at(from, &staging.fd, temporary_name) {
Ok(()) => {
match nix::fcntl::renameat(&staging.fd, temporary_name, &parent.fd, name)
.wrap_err_with(|| format!("failed to install {}", to.display()))
{
Ok(()) => {
remove_private_staging_dir(&parent, &staging, staging_name.as_ref())?;
Ok(())
}
Err(err) => {
remove_all_at(&staging.fd, temporary_name).wrap_err_with(|| {
format!(
"failed to clean up temporary generic artifact after rename failed: {err:#}"
)
})?;View on GitHub (pinned to 9dcfcaa0dc)
Solutions
- Remove mise's brew-cask staging/cache leftovers owned by the other user (under the mise data prefix and the system-brew cache dir), then retry
- Pick one identity (always root, or always your user) for mise cask installs on a given machine
- If the directory is one you did not create, treat it as suspicious and investigate before deleting
Defensive patterns
Strategy: validation
Validate before calling
use std::os::unix::fs::MetadataExt;
fn staging_dir_private(p: &Path) -> Result<bool> {
let m = p.symlink_metadata()?;
Ok(m.uid() == nix::unistd::geteuid().as_raw() && m.mode() & 0o077 == 0)
} Try / catch
Catch 'temporary artifact directory is not private', identify and remove the foreign staging directory, then retry once; if it reappears without explanation, stop and investigate for tampering.
Prevention
- Run mise cask installs consistently as the same user (never alternate root/non-root)
- Don't share mise data/cache dirs across UIDs on multi-user machines
When it happens
Trigger: A stale staging directory left by a previous mise run under a different user (ran with sudo once, then without, or vice versa), or another process creating the staging name with group/other access inside the trusted parent.
Common situations: Switching between root and non-root mise runs on the same machine; shared or kiosk Macs; CI runners reusing $HOME across jobs with different UIDs.
Related errors
- brew-cask: refusing elevated operation through mutable direc
- brew-cask: invalid {kind} '{value}'
- brew-cask: staged symlink path escaped extraction root: {}
- brew-cask: refusing generic artifact source outside the extr
- brew-cask: refusing to stage generic artifact through a path
AI-assisted analysis of jdx/mise@9dcfcaa0dc (2026-08-17).
Data as JSON: /api/errors/3fcbbcf41f9ba03a.
Report an issue: GitHub.