From 57f5be6799c770f956245e523475a3c4c4141cd8 Mon Sep 17 00:00:00 2001 From: jdx <216188+jdx@users.noreply.github.com> Date: Thu, 26 Feb 2026 12:14:02 +0000 Subject: [PATCH] fix(install): prevent --locked mode from modifying or bypassing lockfile In --locked mode, `mise lock` now refuses to run, tool resolution fails with a clear error when a config-sourced tool is missing from the lockfile, and `mise use @latest` no longer bypasses the lockfile. Closes #8305 Co-Authored-By: Claude Opus 4.6 --- e2e/lockfile/test_lockfile_locked_mode | 18 ++++++++++++++++++ src/cli/lock.rs | 7 ++++++- src/cli/use.rs | 2 +- src/toolset/tool_version.rs | 15 ++++++++++++++- 4 files changed, 39 insertions(+), 3 deletions(-) diff --git a/e2e/lockfile/test_lockfile_locked_mode b/e2e/lockfile/test_lockfile_locked_mode index d0a013d635..c7e79f4f1c 100644 --- a/e2e/lockfile/test_lockfile_locked_mode +++ b/e2e/lockfile/test_lockfile_locked_mode @@ -80,5 +80,23 @@ EOF assert_contains "mise install --dry-run 2>&1" "would install" +# --- Test 7: mise lock refuses to run in --locked mode --- +export MISE_LOCKFILE=1 + +cat <<'EOF' >mise.toml +[tools] +jq = "1.7.1" +EOF + +cat <mise.lock +[[tools.jq]] +version = "1.7.1" +backend = "aqua:jqlang/jq" +"platforms.$PLATFORM" = { url = "https://example.com/jq-1.7.1.tar.gz" } +EOF + +assert_fail_contains "mise lock --locked 2>&1" "mise lock is disabled in --locked mode" +MISE_LOCKED=1 assert_fail_contains "mise lock 2>&1" "mise lock is disabled in --locked mode" + # Restore for any subsequent tests export MISE_LOCKFILE=1 diff --git a/src/cli/lock.rs b/src/cli/lock.rs index 2a0dfad0f9..c93832a36a 100644 --- a/src/cli/lock.rs +++ b/src/cli/lock.rs @@ -10,7 +10,7 @@ use crate::toolset::Toolset; use crate::ui::multi_progress_report::MultiProgressReport; use crate::{cli::args::ToolArg, config::Settings}; use console::style; -use eyre::Result; +use eyre::{Result, bail}; use tokio::sync::Semaphore; use tokio::task::JoinSet; @@ -52,6 +52,11 @@ pub struct Lock { impl Lock { pub async fn run(self) -> Result<()> { let settings = Settings::get(); + if settings.locked { + bail!( + "mise lock is disabled in --locked mode\nhint: Remove --locked or unset MISE_LOCKED=1" + ); + } let config = Config::get().await?; let ts = config.get_toolset().await?; diff --git a/src/cli/use.rs b/src/cli/use.rs index be8e181162..dd6acd70ae 100644 --- a/src/cli/use.rs +++ b/src/cli/use.rs @@ -145,7 +145,7 @@ impl Use { .cloned() .map(|t| match t.tvr { Some(tvr) => { - if tvr.version() == "latest" { + if tvr.version() == "latest" && !Settings::get().locked { // user specified `@latest` so we should resolve the latest version // TODO: this should only happen on this tool, not all of them resolve_options.latest_versions = true; diff --git a/src/toolset/tool_version.rs b/src/toolset/tool_version.rs index c9c29ab919..c7c6f13edb 100644 --- a/src/toolset/tool_version.rs +++ b/src/toolset/tool_version.rs @@ -15,7 +15,7 @@ use crate::lockfile::{CondaPackageInfo, LockfileTool, PlatformInfo}; use crate::toolset::{ToolRequest, ToolVersionOptions, tool_request}; use console::style; use dashmap::DashMap; -use eyre::Result; +use eyre::{Result, bail}; use jiff::Timestamp; #[cfg(windows)] use path_absolutize::Absolutize; @@ -210,6 +210,19 @@ impl ToolVersion { { return Ok(Self::from_lockfile(request.clone(), lt)); } + let settings = Settings::get(); + if settings.locked + && opts.use_locked_version + && settings.lockfile_enabled() + && !has_linked_version(request.ba()) + && request.source().path().is_some() + { + bail!( + "{}@{} is not in the lockfile\nhint: Run `mise install` without --locked to update the lockfile", + request.ba().short, + request.version() + ); + } match v.split_once(':') { Some((ref_type @ ("ref" | "tag" | "branch" | "rev"), r)) => {