jdx/mise · error

lockfile symlink changed during preparation; retry the…

Error message

lockfile symlink {} changed during preparation; retry the command

What it means

mise's lockfile generation resolves tools against the target of the `source_symlink` (e.g. a versioned lockfile symlink pointing at the active lockfile deployment). Before publishing sidecar files it rechecks that the symlink still points to the same canonical target it saw when preparation started. If the symlink was removed, replaced, or re-pointed mid-run, it aborts before publishing anything so a stale or foreign lockfile is not augmented, and asks the user to rerun.

Solutions

  1. Rerun the exact same mise command; the check is intentionally retry-safe.
  2. Ensure only one mise process mutates the lockfile at a time (serialize installs and `mise lock` invocations in scripts/CI).
  3. If a migration or upgrade switched the symlink deliberately, run the new command against the current deployment instead of the interrupted one.

Example fix

// before: concurrent commands race on the symlink
mise install &  mise lock --upgrade & wait
// after: serialize lockfile-mutating commands
mise lock --upgrade && mise install
Defensive patterns

Strategy: retry

Validate before calling

# before running mise, confirm the lockfile symlink is stable
readlink mise.lock && readlink mise.lock  # same target twice => stable

Prevention

When it happens

Trigger: Calling any mise command that publishes lockfile sidecars (install/lock resolution with lockfile_enabled) while another process switches the source lockfile symlink between read and publish; concurrent `mise lock --upgrade` or monorepo migration in another terminal; an editor/CI job swapping the symlink target during a long install.

Common situations: Two concurrent mise commands (e.g. `mise install` and `mise lock --upgrade`) racing on the same project; a deployment script rotating the lockfile symlink while a build agent installs tools; manual `ln -sfn` edits during a running install.

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/527b39c7e83c89db. Report an issue: GitHub.

Appendix: source

Thrown at src/lockfile.rs:1975

pub(crate) struct GraphCleanup(graph::SidecarWrites);
impl GraphCleanup {
    pub(crate) fn prune(self) -> Result<()> {
        self.0.prune()
    }
}
impl PreparedWrite {
    pub(crate) fn mutation_paths(&self) -> impl Iterator<Item = &PathBuf> {
        self.sidecars.files.iter().map(|(path, _)| path)
    }
    pub(crate) fn publish_deferred(self) -> Result<GraphCleanup> {
        // Detect a deployment switch during preparation before publishing any
        // sidecars. This is a recheck, not a lock against external symlink edits.
        if let Some(path) = &self.source_symlink
            && (!path.is_symlink()
                || !fs::canonicalize(path).is_ok_and(|target| target == self.target))
        {
            bail!(
                "lockfile symlink {} changed during preparation; retry the command",
                display_path(path)
            );
        }
        self.sidecars.publish_files()?;
        if let Some(tmp) = self.tmp {
            persist_lockfile_tmp(tmp, &self.target)?;
        }
        invalidate_caches();
        Ok(GraphCleanup(self.sidecars))
    }
    pub(crate) fn publish(self) -> Result<()> {
        self.publish_deferred()?.prune()
    }
}

/// Determines the lockfile path for a given config file path
/// Returns (lockfile_path, is_local)

View on GitHub (pinned to 533346cc37)