From 471ca9b16831d0cee12e5dc498cb268eb7f57d00 Mon Sep 17 00:00:00 2001 From: default <216188+jdx@users.noreply.github.com> Date: Mon, 17 Aug 2026 20:02:53 +0000 Subject: [PATCH 1/3] feat(config): implicitly trust config for execution commands --- docs/cli/trust.md | 20 +++---- docs/cli/untrust.md | 2 +- docs/faq.md | 21 ++++---- docs/getting-started.md | 12 +++-- docs/paranoid.md | 30 ++++++----- docs/public/llms.txt | 2 +- e2e/cli/test_hook_env_untrusted_once_per_dir | 11 ++-- e2e/cli/test_prune_tracked_config_trust | 3 +- e2e/config/test_non_paranoid_implicit_trust | 48 +++++++++++++++++ e2e/config/test_trust_safe_config | 20 ++++--- e2e/config/test_trust_safe_config_tracked | 4 +- e2e/tasks/test_task_untrusted_config_error | 6 +-- mise.usage.kdl | 22 ++++---- src/cli/mod.rs | 57 ++++++++++++++++++-- src/cli/trust.rs | 20 +++---- src/cli/untrust.rs | 2 +- src/config/config_file/mod.rs | 42 +++++++++++++++ 17 files changed, 239 insertions(+), 83 deletions(-) create mode 100644 e2e/config/test_non_paranoid_implicit_trust diff --git a/docs/cli/trust.md b/docs/cli/trust.md index 3a96a9713a5..0127feb7357 100644 --- a/docs/cli/trust.md +++ b/docs/cli/trust.md @@ -8,17 +8,17 @@ Marks a config file as trusted This means mise is allowed to parse the file when it needs to read config -that may execute code or affect the environment. mise checks trust before -parsing `mise.toml`. Without trust, mise may prompt, skip the config in some -discovery paths, fail with an untrusted-config error when it cannot prompt, -or assume trust in detected CI unless paranoid mode is enabled. +that may execute code or affect the environment. Without trust, mise may +prompt, skip the config in some discovery paths, or fail with an +untrusted-config error when it cannot prompt. + +In normal mode, commands that execute project-defined behavior (`mise run`, +`mise install`, `mise exec`, and `mise watch`) automatically trust their +active config. Paranoid mode always requires explicit, content-bound trust. Safe config files do not require trust: files that only contain -`min_version`, `[tools]` entries with plain version strings (or arrays -of them), and `[tasks]` (no templates and no tool options) are loaded -without prompting, since nothing in them executes code at load time — -tools install and tasks run only on explicit commands like `mise install` -or `mise run`. +`min_version`, `[tools]` entries with plain version strings (or arrays of +them), and `[tasks]` without templates or tool options. Trust is shared across git worktrees: a config file inside a linked worktree is trusted when the equivalent path in the repository's main @@ -51,7 +51,7 @@ Does not trust or untrust any files. ### `--untrust` -No longer trust this config, will prompt in the future +Remove explicit trust for this config Examples: diff --git a/docs/cli/untrust.md b/docs/cli/untrust.md index 73851854e53..35041bd6b96 100644 --- a/docs/cli/untrust.md +++ b/docs/cli/untrust.md @@ -5,7 +5,7 @@ - **Effect**: modifies state - **Source code**: [`src/cli/untrust.rs`](https://github.com/jdx/mise/blob/main/src/cli/untrust.rs) -No longer trust a config, will prompt in the future +Remove explicit trust for a config ## Arguments diff --git a/docs/faq.md b/docs/faq.md index 58fb1165984..9d6f4e26bc9 100644 --- a/docs/faq.md +++ b/docs/faq.md @@ -246,11 +246,10 @@ mise upgrade --bump node ## My config file is being ignored / `mise trust` issues mise requires you to trust config files that were not created by you. Safe config files — -those that only contain `min_version`, `[tools]` entries with plain version strings (or -arrays of them), and `[tasks]` (no templates and no tool options) — are loaded without trust, since nothing in -them executes code at load time: tools install and tasks run only on explicit commands like -`mise install` or `mise run`. Everything else (env vars, hooks, settings, aliases, templates, -tool options) requires trust. Common issues: +those that only contain `min_version`, plain `[tools]` entries, and `[tasks]` without +templates or tool options — load without trust. In normal mode, `mise run`, `mise install`, +`mise exec`, and `mise watch` automatically trust the active config because they explicitly +execute project-defined behavior. Other unsafe config requires trust. Common issues: - **Accidentally denied trust**: If mise prompted you to trust a file and you said no, it gets added to the ignore list. Check the `ignored-configs` directory in your @@ -259,17 +258,17 @@ tool options) requires trust. Common issues: - **Symlinked configs**: If your config is symlinked (e.g., via GNU Stow), mise may track the symlink target path. Try `mise trust` pointing to the actual file path. - **CI**: In detected CI, mise assumes configs are trusted unless paranoid mode is enabled. -- **Non-interactive mode**: In non-interactive shells outside detected CI, such as IDE extensions or - scripts without a TTY, mise cannot prompt you to trust a config. Commands that directly load an - untrusted `mise.toml` can fail with an untrusted-config error. Commands that discover previously - tracked configs may skip untrusted entries instead. Either run `mise trust` beforehand or set +- **Non-interactive mode**: In a non-interactive shell, such as an IDE extension or script without + a TTY, mise cannot prompt you to trust a config. Commands that directly load an untrusted + `mise.toml` can fail with an untrusted-config error. Commands that discover previously tracked + configs may skip untrusted entries instead. Either run `mise trust` beforehand or set [`trusted_config_paths`](/configuration/settings.html#trusted_config_paths) in your global settings for configs you trust. - **Global config** (`~/.config/mise/config.toml`) should be auto-trusted. If it's not, run `mise trust ~/.config/mise/config.toml` explicitly. -Run `mise doctor` (`mise dr`) to see if any config files are untrusted — it will list them -under "problems". +Run `mise doctor` (`mise dr`) to see if any config files are untrusted — it will +list them under "problems". ## How do idiomatic version files (`.python-version`, `.node-version`, etc.) work? diff --git a/docs/getting-started.md b/docs/getting-started.md index 3f7caa64b15..90c66459bb7 100644 --- a/docs/getting-started.md +++ b/docs/getting-started.md @@ -312,7 +312,7 @@ See [Backends](/dev-tools/backends/) for more ecosystems and details. ## Trusting config files {#trust} -When you or a teammate adds a `mise.toml` to a project, mise will prompt you to trust it before it runs any env directives or hooks: +When you or a teammate adds a `mise.toml` to a project, mise may prompt you to trust it before it runs env directives or hooks: ``` mise ~/my-project/mise.toml is not trusted. Trust it? [y/n] @@ -324,9 +324,11 @@ This is a security measure — config files can execute arbitrary code via `[env mise trust ``` -This only needs to be done once per file. See [`mise trust`](/cli/trust) for more details. +In normal mode, `mise run`, `mise install`, `mise exec`, and `mise watch` +automatically trust the active config because those commands explicitly execute +project-defined behavior. Paranoid mode always requires `mise trust`. -To disable trust prompts entirely, trust the root path: +To disable trust prompts for a path, configure: ```sh mise settings trusted_config_paths=["/"] @@ -335,9 +337,11 @@ mise settings trusted_config_paths=["/"] Or set the environment variable `MISE_TRUSTED_CONFIG_PATHS=/`. ::: tip -`mise use` automatically trusts the file it creates, so you'll only see this prompt when pulling a config someone else wrote or when editing `mise.toml` by hand. +`mise use` automatically trusts the file it creates. ::: +See [`mise trust`](/cli/trust) for more details. + ## 5. Setting environment variables {#environment-variables} Define environment variables in `mise.toml` — they'll be loaded whenever mise is activated or when using `mise exec`: diff --git a/docs/paranoid.md b/docs/paranoid.md index 10c9e111747..e4df3da38b4 100644 --- a/docs/paranoid.md +++ b/docs/paranoid.md @@ -14,26 +14,28 @@ mise settings paranoid=1 ## Config files Normally `mise` will make sure some config files are "trusted" before loading -them. This will prompt you to confirm that you want to load the file, e.g.: +them. This can prompt you to confirm that you want to load the file, e.g.: ```sh -$ mise install -mise ~/src/mise/.tool-versions is not trusted. Trust it [y/n]? +$ mise env +mise ~/src/mise/mise.toml is not trusted. Trust it [y/n]? ``` -In normal mode, mise checks trust before parsing `mise.toml` files because they -can contain behavior that executes code or affects the environment. Some -discovery paths that look at previously tracked configs may skip untrusted files -instead of prompting. Commands that directly need an untrusted config, such as -`mise lock`, can fail with an untrusted-config error when mise cannot prompt. -When mise detects that it is running in CI, configs are assumed to be trusted -unless paranoid mode is enabled. +In normal mode, `mise run`, `mise install`, `mise exec`, and `mise watch` +automatically trust their active config because they explicitly execute +project-defined behavior. Automatic shell activation through `hook-env` does not. -Under paranoid, all config files must be trusted first, including formats that -normally do not require trust. +Other commands check trust before parsing `mise.toml` files because they can +contain behavior that executes code or affects the environment. Some discovery +paths that look at previously tracked configs may skip untrusted files instead +of prompting. Commands that directly need an untrusted config can fail with an +untrusted-config error when mise cannot prompt. When mise detects that it is +running in CI, configs are assumed to be trusted unless paranoid mode is enabled. -Also, in normal mode, a config file only needs to be trusted a single time. -In paranoid, the contents of the file are hashed to check if the file changes. +Under paranoid, all config files must be trusted first, including formats that +normally do not require trust. Automatic trust for execution commands is disabled. +In normal mode, a config file only needs to be trusted a single time. In paranoid, +the contents of the file are hashed to check if it changes. If you change your config file, you'll need to trust it again. Note that global and system config files (e.g., `~/.config/mise/config.toml`) are implicitly trusted and exempt from this check. This allows paranoid mode to be enabled in a global config without requiring a trust prompt for that file itself. diff --git a/docs/public/llms.txt b/docs/public/llms.txt index 94d3da2aaec..d8a669c7a1d 100644 --- a/docs/public/llms.txt +++ b/docs/public/llms.txt @@ -283,7 +283,7 @@ - [mise trust](https://mise.jdx.dev/cli/trust.html): Marks a config file as trusted - [mise uninstall](https://mise.jdx.dev/cli/uninstall.html): Removes installed tool versions - [mise unset](https://mise.jdx.dev/cli/unset.html): Remove environment variable(s) from the config file. -- [mise untrust](https://mise.jdx.dev/cli/untrust.html): No longer trust a config, will prompt in the future +- [mise untrust](https://mise.jdx.dev/cli/untrust.html): Remove explicit trust for a config - [mise unuse](https://mise.jdx.dev/cli/unuse.html): Removes installed tool versions from mise.toml - [mise upgrade](https://mise.jdx.dev/cli/upgrade.html): Upgrades outdated tools - [mise use](https://mise.jdx.dev/cli/use.html): Installs a tool and adds the version to mise.toml. diff --git a/e2e/cli/test_hook_env_untrusted_once_per_dir b/e2e/cli/test_hook_env_untrusted_once_per_dir index 220c54fd06a..048964ef8d9 100644 --- a/e2e/cli/test_hook_env_untrusted_once_per_dir +++ b/e2e/cli/test_hook_env_untrusted_once_per_dir @@ -120,12 +120,13 @@ if ! contains_trust_warning "$quiet_stderr"; then fi cd ../project -MISE_YES=0 mise run make 2>"$explicit_stderr" >/dev/null && { - echo "expected mise run to fail on untrusted config" +if ! MISE_YES=0 mise run make 2>"$explicit_stderr" >/dev/null; then + echo "expected explicit mise run to trust the active config" + cat "$explicit_stderr" exit 1 -} -if ! contains_trust_warning "$explicit_stderr"; then - echo "expected explicit mise run to keep reporting untrusted config" +fi +if contains_trust_warning "$explicit_stderr"; then + echo "expected explicit mise run not to report an untrusted config" cat "$explicit_stderr" exit 1 fi diff --git a/e2e/cli/test_prune_tracked_config_trust b/e2e/cli/test_prune_tracked_config_trust index b4d741cf6fa..2712a0c9826 100644 --- a/e2e/cli/test_prune_tracked_config_trust +++ b/e2e/cli/test_prune_tracked_config_trust @@ -20,7 +20,8 @@ mise -C tool-versions install # Track a plain .tool-versions file, then make it require trust. Tracked-config # loading must skip it without rendering the template. echo "dummy 3.0.0" >templated/.tool-versions -mise -C templated install +mise install dummy@3.0.0 +mise -C templated ls cat <templated/.tool-versions dummy {{ exec(command="touch $marker") }}3.0.0 EOF diff --git a/e2e/config/test_non_paranoid_implicit_trust b/e2e/config/test_non_paranoid_implicit_trust new file mode 100644 index 00000000000..9b5d7cbda44 --- /dev/null +++ b/e2e/config/test_non_paranoid_implicit_trust @@ -0,0 +1,48 @@ +#!/usr/bin/env bash + +# Commands that execute project-defined behavior implicitly trust their active +# config in normal mode. hook-env does not, and paranoid mode always requires +# an explicit, content-bound trust decision. + +export MISE_TRUSTED_CONFIG_PATHS="" +unset CI GITHUB_ACTIONS GITHUB_ACTION 2>/dev/null || true + +mkdir -p install run exec paranoid + +cat <<'EOF' >install/mise.toml +[env] +IMPLICIT_TRUST = "install" +EOF + +assert_not_contains "cd install && MISE_YES=0 mise hook-env -s bash 2>&1 || true" "IMPLICIT_TRUST=install" +assert "cd install && MISE_YES=0 mise install" +assert_contains "cd install && MISE_YES=0 mise hook-env -s bash" "IMPLICIT_TRUST" + +cat <<'EOF' >run/mise.toml +[env] +IMPLICIT_TRUST = "run" + +[tasks.hi] +run = "echo $IMPLICIT_TRUST" +EOF + +assert_contains "cd run && MISE_YES=0 mise run hi" "run" + +cat <<'EOF' >exec/mise.toml +[env] +IMPLICIT_TRUST = "exec" +EOF + +assert_contains "cd exec && MISE_YES=0 mise exec -- sh -c 'echo \$IMPLICIT_TRUST'" "exec" + +cat <<'EOF' >paranoid/mise.toml +[env] +IMPLICIT_TRUST = "paranoid" +EOF + +set +e +output=$(cd paranoid && MISE_PARANOID=1 MISE_YES=0 mise install 2>&1) +status=$? +set -e +[[ $status -ne 0 ]] || fail "paranoid install unexpectedly trusted the config" +echo "$output" | grep -qi "trust" || fail "expected paranoid install to report an untrusted config, got: $output" diff --git a/e2e/config/test_trust_safe_config b/e2e/config/test_trust_safe_config index 59e07c5cc2c..89372e93ec4 100644 --- a/e2e/config/test_trust_safe_config +++ b/e2e/config/test_trust_safe_config @@ -30,8 +30,7 @@ run = "echo task-ran-without-trust" EOF # loads without prompting and without creating a trust marker -MISE_YES=0 mise install tiny -assert_contains "MISE_YES=0 mise ls tiny" "3.1.0" +assert_contains "MISE_YES=0 mise ls" "3.1.0" assert_no_trust_marker "safe config should not be silently trusted" # safe config tool pins still prompt before installing non-registry plugin URLs @@ -46,9 +45,13 @@ set -e [[ $status -ne 0 ]] || fail "expected community plugin install to abort without confirmation" echo "$output" | grep -q "community-developed plugin" || fail "expected community plugin prompt, got: $output" echo "$output" | grep -q "github.com/attacker/evil" || fail "expected attacker plugin URL, got: $output" -assert_no_trust_marker "community plugin prompt should not trust config" +# `mise install` trusts the active config independently of the later plugin +# confirmation. Remove that trust so the remaining checks start untrusted. +assert_contains "mise trust --show" "trusted" +mise untrust +assert_no_trust_marker "mise untrust should remove install's implicit trust" -# config tasks only execute on explicit `mise run`, so they don't need trust +# config tasks execute on explicit `mise run`, which now trusts active config cat <<'EOF' >mise.toml min_version = "2024.1.1" @@ -59,8 +62,11 @@ tiny = "3.1.0" run = "echo task-ran-without-trust" EOF assert_contains "MISE_YES=0 mise run hi" "task-ran-without-trust" +assert_contains "mise trust --show" "trusted" +mise untrust -# template-free file tasks are inert at load time and runnable without trust +# template-free file tasks are inert at load time; explicit `mise run` trusts +# the active config before running them mkdir -p mise-tasks cat <<'EOF' >mise-tasks/filetask #!/usr/bin/env bash @@ -69,7 +75,9 @@ echo file-task-ran-without-trust EOF chmod +x mise-tasks/filetask assert_contains "MISE_YES=0 mise run filetask" "file-task-ran-without-trust" -assert_no_trust_marker "file task should not require or create trust" +assert_contains "mise trust --show" "trusted" +mise untrust +assert_no_trust_marker "mise untrust should remove run's implicit trust" # a template in a file task header renders at load time, so it requires trust cat <<'EOF' >mise-tasks/evil diff --git a/e2e/config/test_trust_safe_config_tracked b/e2e/config/test_trust_safe_config_tracked index 98003349011..0cbc3918cb6 100644 --- a/e2e/config/test_trust_safe_config_tracked +++ b/e2e/config/test_trust_safe_config_tracked @@ -14,8 +14,8 @@ cat <<'EOF' >proj/mise.toml tiny = "3.1.0" EOF -# install from the safe config (loads + tracks it, no trust prompt/marker) -(cd proj && MISE_YES=0 mise install tiny) +# load the safe config so it is tracked, without creating a trust marker +(cd proj && MISE_YES=0 mise ls) # from an unrelated directory, the tracked safe config's pin is still listed cd other || exit 1 diff --git a/e2e/tasks/test_task_untrusted_config_error b/e2e/tasks/test_task_untrusted_config_error index b5a19d43ffa..02517f04f27 100644 --- a/e2e/tasks/test_task_untrusted_config_error +++ b/e2e/tasks/test_task_untrusted_config_error @@ -16,8 +16,8 @@ EOF assert_contains "MISE_YES=0 mise run make" "hello from task" -# Unsafe configs (here: [env]) still require trust. Running a task from one -# should give a helpful error message: +# Paranoid mode requires trust for unsafe configs (here: [env]). Running a task +# from one should give a helpful error message: # 1. Prompt the user to trust the config (when interactive), or # 2. Show a clear "config not trusted" error (not "no tasks defined") cat <mise.toml @@ -29,7 +29,7 @@ run = "echo 'hello from task'" EOF # This test verifies we don't get the misleading "no tasks defined" error -output=$(MISE_YES=0 mise run make 2>&1 || true) +output=$(MISE_PARANOID=1 MISE_YES=0 mise run make 2>&1 || true) # The error should mention "trust" or "untrusted", not just "no tasks defined" if echo "$output" | grep -q "no tasks defined.*Are you in a project directory"; then diff --git a/mise.usage.kdl b/mise.usage.kdl index 49cf8ed9e25..35d29d0b5e5 100644 --- a/mise.usage.kdl +++ b/mise.usage.kdl @@ -4429,17 +4429,17 @@ cmd trust help="Marks a config file as trusted" effect=write { Marks a config file as trusted This means mise is allowed to parse the file when it needs to read config -that may execute code or affect the environment. mise checks trust before -parsing `mise.toml`. Without trust, mise may prompt, skip the config in some -discovery paths, fail with an untrusted-config error when it cannot prompt, -or assume trust in detected CI unless paranoid mode is enabled. +that may execute code or affect the environment. Without trust, mise may +prompt, skip the config in some discovery paths, or fail with an +untrusted-config error when it cannot prompt. + +In normal mode, commands that execute project-defined behavior (`mise run`, +`mise install`, `mise exec`, and `mise watch`) automatically trust their +active config. Paranoid mode always requires explicit, content-bound trust. Safe config files do not require trust: files that only contain -`min_version`, `[tools]` entries with plain version strings (or arrays -of them), and `[tasks]` (no templates and no tool options) are loaded -without prompting, since nothing in them executes code at load time — -tools install and tasks run only on explicit commands like `mise install` -or `mise run`. +`min_version`, `[tools]` entries with plain version strings (or arrays of +them), and `[tasks]` without templates or tool options. Trust is shared across git worktrees: a config file inside a linked worktree is trusted when the equivalent path in the repository's main @@ -4469,7 +4469,7 @@ and common build/dependency directories (node_modules, vendor, target, dist, bui Show the trusted status of config files from the current directory and its parents. Does not trust or untrust any files. """# - flag --untrust help="No longer trust this config, will prompt in the future" + flag --untrust help="Remove explicit trust for this config" arg "[CONFIG_FILE]" help="The config file to trust" required=#false } cmd uninstall help="Removes installed tool versions" effect=destructive { @@ -4534,7 +4534,7 @@ Environment variable(s) to remove e.g.: NODE_ENV """# required=#false var=#true } -cmd untrust help="No longer trust a config, will prompt in the future" effect=write { +cmd untrust help="Remove explicit trust for a config" effect=write { arg "[CONFIG_FILE]" help="The config file to untrust" required=#false } cmd unuse help="Removes installed tool versions from mise.toml" effect=destructive { diff --git a/src/cli/mod.rs b/src/cli/mod.rs index a0dc92f1f80..0f55d857c7c 100644 --- a/src/cli/mod.rs +++ b/src/cli/mod.rs @@ -1,4 +1,4 @@ -use crate::config::{Config, Settings}; +use crate::config::{Config, Settings, config_file}; use crate::task::TaskOutput; use crate::ui::{self, ctrlc}; use crate::{Result, backend, request_exit}; @@ -288,6 +288,13 @@ pub(crate) fn expand_deferred_subcommands(mut command: clap::Command) -> clap::C } impl Commands { + fn implicitly_trusts_active_config(&self) -> bool { + matches!( + self, + Self::Exec(_) | Self::Install(_) | Self::Run(_) | Self::Watch(_) + ) + } + pub async fn run(self) -> Result<()> { match self { Self::Activate(cmd) => cmd.run(), @@ -772,10 +779,19 @@ impl Cli { ::from_arg_matches(&matches) .map_err(|err| clap_error(err.format(&mut Cli::command()))) })?; + config_file::set_implicitly_trust_active_config( + cli.command + .as_ref() + .is_some_and(Commands::implicitly_trusts_active_config) + || cli.task.is_some(), + ); // Validate --cd path BEFORE Settings processes it and changes the directory validate_cd_path(&cli.cd)?; measure!("add_cli_matches", { Settings::add_cli_matches(&cli) }); let _ = measure!("settings", { Settings::try_get() }); + measure!("trust_active_config", { + config_file::trust_active_config()? + }); measure!("logger", { logger::init() }); if !print_version { measure!("registry::refresh", { crate::registry::refresh().await }); @@ -982,6 +998,43 @@ fn validate_cd_path(cd: &Option) -> Result<()> { #[cfg(test)] mod tests { + use clap::Parser; + + use super::*; + + #[test] + fn test_commands_that_implicitly_trust_active_config() { + let trusting = [ + vec!["mise", "run", "task"], + vec!["mise", "install"], + vec!["mise", "exec", "--", "true"], + vec!["mise", "watch", "task"], + ]; + for args in trusting { + let cli = Cli::try_parse_from(&args).unwrap(); + assert!( + cli.command + .as_ref() + .is_some_and(Commands::implicitly_trusts_active_config), + "expected {args:?} to imply config trust" + ); + } + + let non_trusting = [ + vec!["mise", "hook-env", "-s", "bash"], + vec!["mise", "env"], + vec!["mise", "ls"], + ]; + for args in non_trusting { + let cli = Cli::try_parse_from(&args).unwrap(); + assert!( + !cli.command + .as_ref() + .is_some_and(Commands::implicitly_trusts_active_config), + "expected {args:?} not to imply config trust" + ); + } + } /// Guards [`GLOBAL_FLAGS_WITH_VALUES`]. It is hardcoded so that startup does /// not have to build the clap tree; this keeps it honest. If you added a @@ -1001,8 +1054,6 @@ mod tests { "GLOBAL_FLAGS_WITH_VALUES is stale; clap reports {derived:?}" ); } - use super::*; - #[tokio::test] async fn exit_signal_drops_command_future_before_returning() { struct DropGuard(std::sync::Arc); diff --git a/src/cli/trust.rs b/src/cli/trust.rs index 49606f37675..f006ff33727 100644 --- a/src/cli/trust.rs +++ b/src/cli/trust.rs @@ -15,17 +15,17 @@ use itertools::Itertools; /// Marks a config file as trusted /// /// This means mise is allowed to parse the file when it needs to read config -/// that may execute code or affect the environment. mise checks trust before -/// parsing `mise.toml`. Without trust, mise may prompt, skip the config in some -/// discovery paths, fail with an untrusted-config error when it cannot prompt, -/// or assume trust in detected CI unless paranoid mode is enabled. +/// that may execute code or affect the environment. Without trust, mise may +/// prompt, skip the config in some discovery paths, or fail with an +/// untrusted-config error when it cannot prompt. +/// +/// In normal mode, commands that execute project-defined behavior (`mise run`, +/// `mise install`, `mise exec`, and `mise watch`) automatically trust their +/// active config. Paranoid mode always requires explicit, content-bound trust. /// /// Safe config files do not require trust: files that only contain -/// `min_version`, `[tools]` entries with plain version strings (or arrays -/// of them), and `[tasks]` (no templates and no tool options) are loaded -/// without prompting, since nothing in them executes code at load time — -/// tools install and tasks run only on explicit commands like `mise install` -/// or `mise run`. +/// `min_version`, `[tools]` entries with plain version strings (or arrays of +/// them), and `[tasks]` without templates or tool options. /// /// Trust is shared across git worktrees: a config file inside a linked /// worktree is trusted when the equivalent path in the repository's main @@ -54,7 +54,7 @@ pub struct Trust { #[clap(long, verbatim_doc_comment)] show: bool, - /// No longer trust this config, will prompt in the future + /// Remove explicit trust for this config #[clap(long)] untrust: bool, } diff --git a/src/cli/untrust.rs b/src/cli/untrust.rs index a6d823c9b28..a014f4ed62c 100644 --- a/src/cli/untrust.rs +++ b/src/cli/untrust.rs @@ -5,7 +5,7 @@ use clap::ValueHint; use super::trust; -/// No longer trust a config, will prompt in the future +/// Remove explicit trust for a config #[derive(Debug, clap::Args)] #[clap(verbatim_doc_comment)] pub struct Untrust { diff --git a/src/config/config_file/mod.rs b/src/config/config_file/mod.rs index 6e0a7c5f8b8..babc2e30ef7 100644 --- a/src/config/config_file/mod.rs +++ b/src/config/config_file/mod.rs @@ -2,6 +2,7 @@ use std::ffi::OsStr; use std::fmt::{Debug, Display}; use std::hash::Hash; use std::path::{Path, PathBuf}; +use std::sync::atomic::{AtomicBool, Ordering}; use std::sync::{Mutex, Once}; use std::{ collections::{BTreeMap, BTreeSet, HashMap, HashSet}, @@ -374,6 +375,34 @@ pub fn is_path_trusted(path: &Path) -> bool { is_trusted(&config_trust_root(path)) || is_trusted(path) } +static IMPLICITLY_TRUST_ACTIVE_CONFIG: AtomicBool = AtomicBool::new(false); + +pub fn set_implicitly_trust_active_config(enabled: bool) { + IMPLICITLY_TRUST_ACTIVE_CONFIG.store(enabled, Ordering::Relaxed); +} + +pub fn trust_active_config() -> Result<()> { + if !IMPLICITLY_TRUST_ACTIVE_CONFIG.load(Ordering::Relaxed) { + return Ok(()); + } + let Ok(settings) = Settings::try_get() else { + return Ok(()); + }; + if settings.paranoid || Settings::safe_mode() { + return Ok(()); + } + for path in config::load_config_paths(&config::DEFAULT_CONFIG_FILENAMES, false) { + if config::is_global_config(&path) { + continue; + } + let config_root = config_trust_root(&path); + if !is_trusted(&config_root) { + trust(&config_root)?; + } + } + Ok(()) +} + pub fn trust_check(path: &Path) -> eyre::Result<()> { // In safe mode, config is inert (no code execution, no env injection — see // MISE_SAFE / the `safe` setting), so loading an untrusted config is @@ -382,6 +411,19 @@ pub fn trust_check(path: &Path) -> eyre::Result<()> { if Settings::safe_mode() { return Ok(()); } + // Commands that execute project-defined behavior are an explicit signal + // to trust their active config in normal mode. Persist the decision here + // so unsafe config can load before the command starts; safe config is + // persisted by `trust_active_config` after settings initialization. + if IMPLICITLY_TRUST_ACTIVE_CONFIG.load(Ordering::Relaxed) + && Settings::try_get().is_ok_and(|settings| !settings.paranoid) + { + let config_root = config_trust_root(path); + if is_path_trusted(path) || (!is_ignored(&config_root) && !is_ignored(path)) { + trust(&config_root)?; + return Ok(()); + } + } static MUTEX: Mutex<()> = Mutex::new(()); let _lock = MUTEX.lock().unwrap(); // Prevent multiple checks at once so we don't prompt multiple times for the same path let config_root = config_trust_root(path); From d4f3bb6f5cf9e09ede704d41c31217282870be19 Mon Sep 17 00:00:00 2001 From: default <216188+jdx@users.noreply.github.com> Date: Mon, 17 Aug 2026 21:16:46 +0000 Subject: [PATCH 2/3] fix(config): preserve implicit trust boundaries --- docs/faq.md | 12 ++++++----- e2e/config/test_non_paranoid_implicit_trust | 24 ++++++++++++++++++++- man/man1/mise.1 | 24 ++++++++++----------- src/config/config_file/mod.rs | 4 ++++ 4 files changed, 46 insertions(+), 18 deletions(-) diff --git a/docs/faq.md b/docs/faq.md index 9d6f4e26bc9..fe3f192000a 100644 --- a/docs/faq.md +++ b/docs/faq.md @@ -246,8 +246,9 @@ mise upgrade --bump node ## My config file is being ignored / `mise trust` issues mise requires you to trust config files that were not created by you. Safe config files — -those that only contain `min_version`, plain `[tools]` entries, and `[tasks]` without -templates or tool options — load without trust. In normal mode, `mise run`, `mise install`, +those that only contain `min_version`, `[tools]` entries whose values are plain version +strings or arrays of strings, and `[tasks]` without templates — load without trust. Tool-option +tables and other top-level settings require trust. In normal mode, `mise run`, `mise install`, `mise exec`, and `mise watch` automatically trust the active config because they explicitly execute project-defined behavior. Other unsafe config requires trust. Common issues: @@ -259,9 +260,10 @@ execute project-defined behavior. Other unsafe config requires trust. Common iss symlink target path. Try `mise trust` pointing to the actual file path. - **CI**: In detected CI, mise assumes configs are trusted unless paranoid mode is enabled. - **Non-interactive mode**: In a non-interactive shell, such as an IDE extension or script without - a TTY, mise cannot prompt you to trust a config. Commands that directly load an untrusted - `mise.toml` can fail with an untrusted-config error. Commands that discover previously tracked - configs may skip untrusted entries instead. Either run `mise trust` beforehand or set + a TTY, mise cannot prompt you to trust a config. Outside normal-mode `mise run`, `mise install`, + `mise exec`, and `mise watch`, commands that directly load an untrusted `mise.toml` can fail with + an untrusted-config error. Commands that discover previously tracked configs may skip untrusted + entries instead. Either run `mise trust` beforehand or set [`trusted_config_paths`](/configuration/settings.html#trusted_config_paths) in your global settings for configs you trust. - **Global config** (`~/.config/mise/config.toml`) should be auto-trusted. If it's not, run diff --git a/e2e/config/test_non_paranoid_implicit_trust b/e2e/config/test_non_paranoid_implicit_trust index 9b5d7cbda44..16d92acf397 100644 --- a/e2e/config/test_non_paranoid_implicit_trust +++ b/e2e/config/test_non_paranoid_implicit_trust @@ -7,7 +7,7 @@ export MISE_TRUSTED_CONFIG_PATHS="" unset CI GITHUB_ACTIONS GITHUB_ACTION 2>/dev/null || true -mkdir -p install run exec paranoid +mkdir -p install run exec ignored ci paranoid cat <<'EOF' >install/mise.toml [env] @@ -27,6 +27,8 @@ run = "echo $IMPLICIT_TRUST" EOF assert_contains "cd run && MISE_YES=0 mise run hi" "run" +assert_contains "cd run && MISE_YES=0 mise trust --show" "trusted" +assert "cd run && mise untrust" cat <<'EOF' >exec/mise.toml [env] @@ -34,6 +36,26 @@ IMPLICIT_TRUST = "exec" EOF assert_contains "cd exec && MISE_YES=0 mise exec -- sh -c 'echo \$IMPLICIT_TRUST'" "exec" +assert_contains "cd exec && MISE_YES=0 mise trust --show" "trusted" +assert "cd exec && mise untrust" + +cat <<'EOF' >ignored/mise.toml +[tools] +dummy = "1.0.0" +EOF + +assert "mise -C ignored trust --ignore mise.toml" +assert "mise -C ignored install" +assert_contains "mise -C ignored trust --show mise.toml" "ignored" + +cat <<'EOF' >ci/mise.toml +[env] +IMPLICIT_TRUST = "ci" +EOF + +ci_state="$PWD/ci-state" +assert "CI=1 MISE_STATE_DIR='$ci_state' mise -C ci install" +assert_fail "find '$ci_state/trusted-configs' -mindepth 1 \( -type f -o -type l \) 2>/dev/null | grep -q ." cat <<'EOF' >paranoid/mise.toml [env] diff --git a/man/man1/mise.1 b/man/man1/mise.1 index 330b28b90db..8baa677f5e1 100644 --- a/man/man1/mise.1 +++ b/man/man1/mise.1 @@ -718,7 +718,7 @@ Removes installed tool versions Remove environment variable(s) from the config file. .TP \fBuntrust\fR -No longer trust a config, will prompt in the future +Remove explicit trust for a config .TP \fBunuse\fR Removes installed tool versions from mise.toml @@ -4580,17 +4580,17 @@ All arguments after the stub file path will be forwarded to the underlying tool. Marks a config file as trusted This means mise is allowed to parse the file when it needs to read config -that may execute code or affect the environment. mise checks trust before -parsing `mise.toml`. Without trust, mise may prompt, skip the config in some -discovery paths, fail with an untrusted\-config error when it cannot prompt, -or assume trust in detected CI unless paranoid mode is enabled. +that may execute code or affect the environment. Without trust, mise may +prompt, skip the config in some discovery paths, or fail with an +untrusted\-config error when it cannot prompt. + +In normal mode, commands that execute project\-defined behavior (`mise run`, +`mise install`, `mise exec`, and `mise watch`) automatically trust their +active config. Paranoid mode always requires explicit, content\-bound trust. Safe config files do not require trust: files that only contain -`min_version`, `[tools]` entries with plain version strings (or arrays -of them), and `[tasks]` (no templates and no tool options) are loaded -without prompting, since nothing in them executes code at load time — -tools install and tasks run only on explicit commands like `mise install` -or `mise run`. +`min_version`, `[tools]` entries with plain version strings (or arrays of +them), and `[tasks]` without templates or tool options. Trust is shared across git worktrees: a config file inside a linked worktree is trusted when the equivalent path in the repository's main @@ -4616,7 +4616,7 @@ Show the trusted status of config files from the current directory and its paren Does not trust or untrust any files. .TP \fB\-\-untrust\fR -No longer trust this config, will prompt in the future +Remove explicit trust for this config \fBArguments:\fR .PP .TP @@ -4673,7 +4673,7 @@ Use the global config file Environment variable(s) to remove e.g.: NODE_ENV .SH "MISE UNTRUST" -No longer trust a config, will prompt in the future +Remove explicit trust for a config .PP \fBUsage:\fR mise untrust [] .PP diff --git a/src/config/config_file/mod.rs b/src/config/config_file/mod.rs index babc2e30ef7..2a911dd741f 100644 --- a/src/config/config_file/mod.rs +++ b/src/config/config_file/mod.rs @@ -396,6 +396,9 @@ pub fn trust_active_config() -> Result<()> { continue; } let config_root = config_trust_root(&path); + if is_ignored(&config_root) || is_ignored(&path) { + continue; + } if !is_trusted(&config_root) { trust(&config_root)?; } @@ -416,6 +419,7 @@ pub fn trust_check(path: &Path) -> eyre::Result<()> { // so unsafe config can load before the command starts; safe config is // persisted by `trust_active_config` after settings initialization. if IMPLICITLY_TRUST_ACTIVE_CONFIG.load(Ordering::Relaxed) + && !ci_info::is_ci() && Settings::try_get().is_ok_and(|settings| !settings.paranoid) { let config_root = config_trust_root(path); From 56a1229f07fd7e388163eea6c694fcab26431665 Mon Sep 17 00:00:00 2001 From: default <216188+jdx@users.noreply.github.com> Date: Mon, 17 Aug 2026 23:22:53 +0000 Subject: [PATCH 3/3] docs(config): clarify implicit trust behavior --- docs/cli/trust.md | 7 ++++--- docs/faq.md | 15 ++++++++------- docs/getting-started.md | 7 ++++--- docs/paranoid.md | 7 ++++--- man/man1/mise.1 | 7 ++++--- mise.usage.kdl | 7 ++++--- src/cli/trust.rs | 7 ++++--- 7 files changed, 32 insertions(+), 25 deletions(-) diff --git a/docs/cli/trust.md b/docs/cli/trust.md index 0127feb7357..0bf1eaec23f 100644 --- a/docs/cli/trust.md +++ b/docs/cli/trust.md @@ -13,10 +13,11 @@ prompt, skip the config in some discovery paths, or fail with an untrusted-config error when it cannot prompt. In normal mode, commands that execute project-defined behavior (`mise run`, -`mise install`, `mise exec`, and `mise watch`) automatically trust their -active config. Paranoid mode always requires explicit, content-bound trust. +naked task invocations such as `mise `, `mise install`, `mise exec`, +and `mise watch`) automatically trust their active config. Paranoid mode +requires explicit, content-bound trust for every non-global config. -Safe config files do not require trust: files that only contain +In normal mode, safe config files do not require trust: files that only contain `min_version`, `[tools]` entries with plain version strings (or arrays of them), and `[tasks]` without templates or tool options. diff --git a/docs/faq.md b/docs/faq.md index fe3f192000a..9b9e883d984 100644 --- a/docs/faq.md +++ b/docs/faq.md @@ -248,9 +248,10 @@ mise upgrade --bump node mise requires you to trust config files that were not created by you. Safe config files — those that only contain `min_version`, `[tools]` entries whose values are plain version strings or arrays of strings, and `[tasks]` without templates — load without trust. Tool-option -tables and other top-level settings require trust. In normal mode, `mise run`, `mise install`, -`mise exec`, and `mise watch` automatically trust the active config because they explicitly -execute project-defined behavior. Other unsafe config requires trust. Common issues: +tables and other top-level settings require trust. In normal mode, `mise run`, naked task +invocations such as `mise `, `mise install`, `mise exec`, and `mise watch` automatically +trust the active config because they explicitly execute project-defined behavior. Other unsafe +config requires trust. Common issues: - **Accidentally denied trust**: If mise prompted you to trust a file and you said no, it gets added to the ignore list. Check the `ignored-configs` directory in your @@ -260,10 +261,10 @@ execute project-defined behavior. Other unsafe config requires trust. Common iss symlink target path. Try `mise trust` pointing to the actual file path. - **CI**: In detected CI, mise assumes configs are trusted unless paranoid mode is enabled. - **Non-interactive mode**: In a non-interactive shell, such as an IDE extension or script without - a TTY, mise cannot prompt you to trust a config. Outside normal-mode `mise run`, `mise install`, - `mise exec`, and `mise watch`, commands that directly load an untrusted `mise.toml` can fail with - an untrusted-config error. Commands that discover previously tracked configs may skip untrusted - entries instead. Either run `mise trust` beforehand or set + a TTY, mise cannot prompt you to trust a config. Outside normal-mode `mise run`, `mise `, + `mise install`, `mise exec`, and `mise watch`, commands that directly load an untrusted + `mise.toml` can fail with an untrusted-config error. Commands that discover previously tracked + configs may skip untrusted entries instead. Either run `mise trust` beforehand or set [`trusted_config_paths`](/configuration/settings.html#trusted_config_paths) in your global settings for configs you trust. - **Global config** (`~/.config/mise/config.toml`) should be auto-trusted. If it's not, run diff --git a/docs/getting-started.md b/docs/getting-started.md index 90c66459bb7..9e47aa3e416 100644 --- a/docs/getting-started.md +++ b/docs/getting-started.md @@ -324,9 +324,10 @@ This is a security measure — config files can execute arbitrary code via `[env mise trust ``` -In normal mode, `mise run`, `mise install`, `mise exec`, and `mise watch` -automatically trust the active config because those commands explicitly execute -project-defined behavior. Paranoid mode always requires `mise trust`. +In normal mode, `mise run`, naked task invocations such as `mise `, `mise install`, +`mise exec`, and `mise watch` automatically trust the active config because those commands +explicitly execute project-defined behavior. Paranoid mode requires `mise trust` for all +non-global configs, including configs that are safe in normal mode. To disable trust prompts for a path, configure: diff --git a/docs/paranoid.md b/docs/paranoid.md index e4df3da38b4..694c8de0b1e 100644 --- a/docs/paranoid.md +++ b/docs/paranoid.md @@ -21,9 +21,10 @@ $ mise env mise ~/src/mise/mise.toml is not trusted. Trust it [y/n]? ``` -In normal mode, `mise run`, `mise install`, `mise exec`, and `mise watch` -automatically trust their active config because they explicitly execute -project-defined behavior. Automatic shell activation through `hook-env` does not. +In normal mode, `mise run`, naked task invocations such as `mise `, +`mise install`, `mise exec`, and `mise watch` automatically trust their active +config because they explicitly execute project-defined behavior. Automatic shell +activation through `hook-env` does not. Other commands check trust before parsing `mise.toml` files because they can contain behavior that executes code or affects the environment. Some discovery diff --git a/man/man1/mise.1 b/man/man1/mise.1 index 8baa677f5e1..ea8c88b349f 100644 --- a/man/man1/mise.1 +++ b/man/man1/mise.1 @@ -4585,10 +4585,11 @@ prompt, skip the config in some discovery paths, or fail with an untrusted\-config error when it cannot prompt. In normal mode, commands that execute project\-defined behavior (`mise run`, -`mise install`, `mise exec`, and `mise watch`) automatically trust their -active config. Paranoid mode always requires explicit, content\-bound trust. +naked task invocations such as `mise `, `mise install`, `mise exec`, +and `mise watch`) automatically trust their active config. Paranoid mode +requires explicit, content\-bound trust for every non\-global config. -Safe config files do not require trust: files that only contain +In normal mode, safe config files do not require trust: files that only contain `min_version`, `[tools]` entries with plain version strings (or arrays of them), and `[tasks]` without templates or tool options. diff --git a/mise.usage.kdl b/mise.usage.kdl index 35d29d0b5e5..b2989ae9019 100644 --- a/mise.usage.kdl +++ b/mise.usage.kdl @@ -4434,10 +4434,11 @@ prompt, skip the config in some discovery paths, or fail with an untrusted-config error when it cannot prompt. In normal mode, commands that execute project-defined behavior (`mise run`, -`mise install`, `mise exec`, and `mise watch`) automatically trust their -active config. Paranoid mode always requires explicit, content-bound trust. +naked task invocations such as `mise `, `mise install`, `mise exec`, +and `mise watch`) automatically trust their active config. Paranoid mode +requires explicit, content-bound trust for every non-global config. -Safe config files do not require trust: files that only contain +In normal mode, safe config files do not require trust: files that only contain `min_version`, `[tools]` entries with plain version strings (or arrays of them), and `[tasks]` without templates or tool options. diff --git a/src/cli/trust.rs b/src/cli/trust.rs index f006ff33727..61030eb132e 100644 --- a/src/cli/trust.rs +++ b/src/cli/trust.rs @@ -20,10 +20,11 @@ use itertools::Itertools; /// untrusted-config error when it cannot prompt. /// /// In normal mode, commands that execute project-defined behavior (`mise run`, -/// `mise install`, `mise exec`, and `mise watch`) automatically trust their -/// active config. Paranoid mode always requires explicit, content-bound trust. +/// naked task invocations such as `mise `, `mise install`, `mise exec`, +/// and `mise watch`) automatically trust their active config. Paranoid mode +/// requires explicit, content-bound trust for every non-global config. /// -/// Safe config files do not require trust: files that only contain +/// In normal mode, safe config files do not require trust: files that only contain /// `min_version`, `[tools]` entries with plain version strings (or arrays of /// them), and `[tasks]` without templates or tool options. ///