jdx/mise · error

cannot safely change

Error message

cannot safely change {} without a recovery preimage: {reason}

What it means

`begin_changes_with` captures pre-change snapshots of configured paths so changes can be reverted. In a scope that `requires_recovery_preimage()` (a strict/recovery mode), a path whose prior state is `PathSnapshot::Unrecorded` (e.g. the file could not be captured, does not exist in STATE, or the capture backend was unavailable) makes a safe change impossible, so it aborts instead of proceeding without a rollback image.

Solutions

  1. Run the non-strict bootstrap path first so the path gets a recorded preimage (the same code falls back to a warning when recovery is not required)
  2. Create/restore the target file at the expected location before beginning changes
  3. Fix read permissions on the path for the user running the tool
  4. Inspect the `{reason}` in the message to identify which capture condition failed and address it directly

Example fix

// before
begin_changes_with(paths)?; // fails in recovery scope
// after
if !path.exists() { fs::create(path)?; }
begin_changes_with(paths)?;
Defensive patterns

Strategy: validation

Validate before calling

// verify each path is capturable before beginning changes
for path in paths {
  let snap = PathSnapshot::capture_with(&dirs::STATE, &path, capture);
  if let PathSnapshot::Unrecorded { reason, .. } = snap {
    if requires_recovery_preimage() {
      return Err(format!("{path}: {reason}"));
    }
  }
}

Try / catch

match begin_changes_with(paths) {
  Err(e) if e.to_string().starts_with("cannot safely change") => {
    // bootstrap/repair the path, then retry once
  }
  other => other,
}

Prevention

When it happens

Trigger: Calling `begin_changes_with` (via `begin_changes`) while in a recovery-requiring scope and a target path's snapshot returns `Unrecorded { reason, .. }` — e.g. the path does not exist yet, permission to read it failed, or no prior recorded state exists in `dirs::STATE`.

Common situations: Running a deployment/recovery operation against files that were never bootstrapped into history state; files deleted or moved between planning and capture; read-permission problems under a different user or sandbox.

Understand the failure class

Background: "X is required", "must be set", "cannot be empty": the missing-required-config error family, from Vertex AI project/location to WeChat keys — this error's family across 18 libraries.

Related errors


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

Appendix: source

Thrown at src/system/history/journal.rs:534

}

/// [`begin_changes`] with a capture depth per path. Fails when an entry
/// cannot be persisted: the mutation must not go ahead without its
/// write-ahead record.
pub(crate) fn begin_changes_with(
    part: &str,
    item: &str,
    paths: impl IntoIterator<Item = (PathBuf, Capture)>,
) -> Result<Vec<PendingChange>> {
    if !super::scope::is_active() {
        return Ok(vec![]);
    }
    let mut pending = vec![];
    for (path, capture) in paths {
        let prior = PathSnapshot::capture_with(&dirs::STATE, &path, capture);
        if let PathSnapshot::Unrecorded { reason, .. } = &prior {
            if super::scope::requires_recovery_preimage() {
                eyre::bail!(
                    "cannot safely change {} without a recovery preimage: {reason}",
                    display_path(&path)
                );
            }
            warn!(
                "bootstrap: temporary recovery is unavailable for {}: {reason}; proceeding with deployment",
                display_path(&path)
            );
        }
        if let Some(seq) = super::scope::record(JournalEntry::PathChanged {
            part: part.to_string(),
            item: item.to_string(),
            path: path.clone(),
            prior,
        })? {
            pending.push(PendingChange { seq, path });
        }
    }

View on GitHub (pinned to 533346cc37)