jdx/mise · error

brew-cask: refusing generic artifact copy outside Homebrew p

Error message

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

What it means

Generic artifacts (plain files a cask copies into place) must be installed strictly inside the Homebrew prefix. validate_generic_copy_target rejects a target that is not under $HOMEBREW_PREFIX, that is the prefix itself (no component beneath it), or whose symlink-resolved root no longer sits under the prefix (path_starts_with_resolved_root). The refusal prevents a cask from writing outside the managed prefix.

Source

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

    )?;
    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 9dcfcaa0dc)

Solutions

  1. Fix the cask artifact stanza so the target is an absolute path under $HOMEBREW_PREFIX (e.g. "$HOMEBREW_PREFIX/etc/foo.conf")
  2. Verify HOMEBREW_PREFIX is set and matches the installation being operated on
  3. Resolve symlinked prefix components (use the realpath of $(brew --prefix)) or replace them with real directories
  4. If the file must genuinely live outside the prefix, install it with a different mechanism: generic artifacts cannot escape the prefix

Example fix

# cask artifact stanza -- before
target: "/etc/mytool.conf"

# after
target: "$HOMEBREW_PREFIX/etc/mytool.conf"
Defensive patterns

Strategy: validation

Validate before calling

use std::path::{Path, Component};

fn generic_target_is_valid(target: &Path, prefix: &Path) -> bool {
    target.is_absolute()
        && target.starts_with(prefix)
        && target.strip_prefix(prefix).is_ok_and(|r| r.components().next().is_some())
        && !target.components().any(|c| matches!(c, Component::ParentDir))
        && std::fs::canonicalize(target).ok()
            .zip(std::fs::canonicalize(prefix).ok())
            .is_some_and(|(t, p)| t.starts_with(p))
}

Type guard

fn is_beneath_homebrew_prefix(target: &std::path::Path) -> bool {
    let prefix = homebrew_prefix();
    target.starts_with(&prefix)
        && target.strip_prefix(&prefix).is_ok_and(|r| r.components().next().is_some())
}

Try / catch

match copy_generic_artifact(&artifact) {
    Err(e) if e.to_string().contains("outside Homebrew prefix") => {
        // rewrite the target under $HOMEBREW_PREFIX in the cask stanza, then retry
        return Err(e.wrap_err("target must live under $HOMEBREW_PREFIX"));
    }
    other => other?,
}

Prevention

When it happens

Trigger: A cask artifact target that resolves to /etc, /Library, ~/, or any path outside $HOMEBREW_PREFIX; a target equal to the prefix with nothing beneath it; a target lexically inside the prefix but reachable through a symlink that resolves elsewhere.

Common situations: HOMEBREW_PREFIX changed or differing from what the cask assumed; prefix path components replaced by symlinks (custom installs like ~/homebrew); casks ported from formula that hardcode absolute system paths.

Related errors


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