jdx/mise · error

brew-cask: refusing generic artifact copy outside Homebrew…

Error message

brew-cask: refusing generic artifact copy outside Homebrew prefix: {}

What it means

validate_generic_copy_target gates the destination of a cask generic artifact copy. The target must start with the Homebrew prefix path, have at least one component strictly below it (the prefix itself is not a valid destination), and pass path_starts_with_resolved_root, which resolves symlinks so a target that only appears to sit under the prefix via a symlink is rejected. Anything else aborts the copy before data moves.

Solutions

  1. Change the artifact target to a path strictly under the Homebrew prefix, e.g. $HOMEBREW_PREFIX/etc/foo.conf
  2. For system locations (/Library, /etc), use the cask's preflight/postflight steps or a pkg artifact instead of a generic artifact
  3. If the file must exist elsewhere, install under the prefix and symlink from a postflight step
  4. Confirm the prefix mise uses (HOMEBREW_PREFIX / mise settings) matches what the cask assumed

Example fix

# before (cask stanza)
artifact "config.plist", target: "/Library/Preferences/com.example.plist"

# after
artifact "config.plist", target: "#{HOMEBREW_PREFIX}/etc/com.example.plist"
Defensive patterns

Strategy: validation

Validate before calling

// Check a generic artifact target before installing.
fn target_under_prefix(target: &std::path::Path, prefix: &std::path::Path) -> bool {
    target.is_absolute()
        && target.starts_with(prefix)
        && target.strip_prefix(prefix).map(|p| p.components().next().is_some()).unwrap_or(false)
        && std::fs::canonicalize(target)
            .or_else(|_| std::fs::canonicalize(target.parent().unwrap_or(prefix)))
            .map(|r| r.starts_with(prefix))
            .unwrap_or(false)
}

Type guard

fn is_safe_generic_target(target: &str, prefix: &Path) -> bool {
    let expanded = target.replace("$HOMEBREW_PREFIX", &prefix.to_string_lossy());
    target_under_prefix(Path::new(&expanded), prefix)
}

Prevention

When it happens

Trigger: A cask artifact target such as /Library/LaunchDaemons/x.plist, /etc/man.conf, ~/Library/x, a target equal to the prefix itself, or a target under the prefix whose symlinked ancestors resolve outside the prefix.

Common situations: Casks written for classic Homebrew ruby behavior that install launchd plists or system configs to absolute paths; prefix mismatch after moving between /usr/local and /opt/homebrew; assuming arbitrary absolute targets are supported.

Related errors


AI-assisted analysis of jdx/mise@6f52dcdf99 (2026-08-22). Data as JSON: /api/errors/69fd493e898b1bf4. Report an issue: GitHub.

Appendix: source

Thrown at src/system/packages/brew/cask.rs:2189

    )?;
    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()
        );
    }
    Ok(())
}

#[cfg(unix)]
struct TrustedOperationParent {
    fd: std::os::fd::OwnedFd,
}

#[cfg(unix)]
impl TrustedOperationParent {
    fn path(&self) -> Result<PathBuf> {
        #[cfg(target_os = "linux")]
        return Ok(
            Path::new("/proc/self/fd").join(std::os::fd::AsRawFd::as_raw_fd(&self.fd).to_string())

View on GitHub (pinned to 6f52dcdf99)