jdx/mise · error

ownership receipt has changed

Error message

ownership receipt has changed

What it means

validate_cask_prune_candidate compares the on-disk ownership receipt with the receipt captured in the prune candidate. If they differ, the cask's state changed since planning (reinstall, version bump, metadata rewrite), so pruning from the stale plan is unsafe and mise bails. This is the TOCTOU guard on the receipt itself.

Source

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

            .get(&target.path)
            .is_some_and(|tokens| tokens.iter().any(|token| token != &candidate.token))
        {
            bail!(
                "artifact target is now claimed by another cask: {}",
                target.path.display()
            );
        }
    }
    Ok(())
}

pub(super) fn validate_cask_prune_candidate(candidate: &CaskPruneCandidate) -> Result<()> {
    if homebrew_metadata_present(&candidate.token)? {
        bail!("Homebrew now owns this cask");
    }
    let receipt = &candidate.receipt;
    if read_receipt(&candidate.version_dir)?.as_ref() != Some(receipt) {
        bail!("ownership receipt has changed");
    }
    if receipt.schema_version != 3 || !receipt.prune_safe || !receipt.pkg_ids.is_empty() {
        bail!("receipt is not marked safe for direct-artifact pruning");
    }
    if !receipt.metadata_only_apps.is_empty() {
        bail!("metadata-only app ownership cannot be proven safely during pruning");
    }
    let records = receipt
        .targets
        .iter()
        .map(|record| (record.path.clone(), record))
        .collect::<BTreeMap<_, _>>();
    let expected = receipt.standard_targets().cloned().collect::<BTreeSet<_>>();
    if expected.is_empty()
        || records.len() != receipt.targets.len()
        || records.len() != expected.len()
        || receipt
            .metadata_only_apps

View on GitHub (pinned to afd2eddd3a)

Solutions

  1. Re-run the prune to rebuild the plan from the current receipt
  2. If the change was unintended, reinstall the cask via mise to restore a known-good receipt
  3. Avoid mutating the cask (mise or brew operations) between planning and pruning

Example fix

// before: plan made, then cask reinstalled, then apply
mise cask prune <token>   # "ownership receipt has changed"
// after: regenerate plan against current state
mise cask prune <token>  # freshly planned, receipts match
Defensive patterns

Strategy: retry

Validate before calling

// ensure the cask was not mutated after planning
// compare current receipt digest with the planned one before applying
assert_receipt_unchanged(&candidate.version_dir, &candidate.receipt)?;

Try / catch

if let Err(e) = prune(plan) {
    if e.to_string().contains("ownership receipt has changed") {
        let fresh_plan = build_fresh_plan(token)?;
        return prune(fresh_plan);
    }
    return Err(e);
}

Prevention

When it happens

Trigger: read_receipt(&candidate.version_dir) returns a receipt that does not compare equal (schema version, targets, pkg_ids, prune_safe flag, etc.) to candidate.receipt, during cask_prune_plan_from_tokens or apply_cask_prune_plan_in.

Common situations: Cask was reinstalled/upgraded between planning and applying the prune; another process rewrote the mise receipt; plan was made long before apply and the cask changed in between.

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@afd2eddd3a (2026-09-09). Data as JSON: /api/errors/53e520a1a3d35304. Report an issue: GitHub.