jdx/mise · error

completion target is not an owned Caskroom symlink

Error message

completion target is not an owned Caskroom symlink: {}

What it means

During cask prune validation, each recorded shell-completion target must still be a symlink whose path sits under one of the default completion directories (bash/fish/zsh/pwsh) and whose link target resolves below the candidate version directory. This guards prune operations against deleting or relinking completions that are no longer owned by this cask's staged version. If any of those three invariants fails, the prune is refused rather than corrupting another package's completion files.

Solutions

  1. Inspect the reported path: if it is no longer owned by the cask, remove it from the prune candidate or fix the receipt so only owned completions are listed.
  2. Restore the symlink so it points below the candidate version_dir (relink to <version_dir>/... under a default completion directory).
  3. Reinstall or re-stage the cask so the completion targets and their recorded fingerprints are regenerated consistently.

Example fix

// before: prune proceeds while user-managed completion file sits at the recorded path
// after: verify and restore ownership before pruning
let meta = std::fs::symlink_metadata(&path)?;
if !meta.file_type().is_symlink() {
    // remove foreign file or drop the path from receipt.completions before pruning
    std::fs::remove_file(&path)?;
}
Defensive patterns

Strategy: validation

Validate before calling

let meta = std::fs::symlink_metadata(&path)?;
let owned = meta.file_type().is_symlink()
    && completion_roots.iter().any(|r| path_is_below(&path, r))
    && symlink_resolves_below(&path, &candidate.version_dir);
if !owned { /* drop from prune plan or restore before calling validate */ }

Type guard

fn is_owned_completion_symlink(path: &Path, roots: &[PathBuf], version_dir: &Path) -> bool {
    std::fs::symlink_metadata(path).map(|m| m.file_type().is_symlink()).unwrap_or(false)
        && roots.iter().any(|r| path_is_below(path, r))
        && symlink_resolves_below(path, version_dir)
}

Prevention

When it happens

Trigger: Calling validate_cask_prune_candidate (via cask_prune_plan_from_tokens or apply_cask_prune_plan_in) when a receipt.completions entry is not a symlink (CaskTargetKind changed), was moved outside the default completion dirs, or now points to a directory outside the candidate version_dir (e.g. user replaced the symlink with a real file or a custom target).

Common situations: A user manually replaced a generated completion symlink with a copied file; a package update relocated completions to a non-default directory; another tool or dotfiles manager (e.g. oh-my-zsh, fisher) owns the completion path; the cask's version directory was renamed so the symlink dangles or points elsewhere.

Understand the failure class

Background: "Invalid state transition" errors: "status must be X, actually Y", "already rejected/charging/uninstalled", "cannot ... while running" — what they mean when a library rejects your call — this error's family across 31 libraries.

Related errors


AI-assisted analysis of jdx/mise@533346cc37 (2026-09-17). Data as JSON: /api/errors/59edd4904e9928ad. Report an issue: GitHub.

Appendix: source

Thrown at src/system/packages/brew/cask/state.rs:1132

    }
    let completion_roots = [
        CompletionShell::Bash,
        CompletionShell::Fish,
        CompletionShell::Zsh,
        CompletionShell::Pwsh,
    ]
    .map(default_completion_dir);
    for path in &receipt.completions {
        let record = records
            .get(path)
            .ok_or_else(|| eyre!("missing completion target record"))?;
        if record.fingerprint.kind != CaskTargetKind::Symlink
            || !completion_roots
                .iter()
                .any(|root| path_is_below(path, root))
            || !symlink_resolves_below(path, &candidate.version_dir)
        {
            bail!(
                "completion target is not an owned Caskroom symlink: {}",
                path.display()
            );
        }
    }
    for record in &receipt.targets {
        if !cask_target_record_matches(record)? {
            bail!("artifact target has changed: {}", record.path.display());
        }
    }
    Ok(())
}

pub(super) fn path_is_below(path: &Path, root: &Path) -> bool {
    path.strip_prefix(root).is_ok_and(|relative| {
        relative.components().next().is_some()
            && !relative
                .components()

View on GitHub (pinned to 533346cc37)