1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
use std::collections::HashSet;
use std::path::PathBuf;
use crate::config::config_file::config_trust_root;
use crate::config::{
ALL_CONFIG_FILES, DEFAULT_CONFIG_FILENAMES, Settings, config_file, config_files_in_dir,
is_global_config,
};
use crate::file::{display_path, remove_file};
use crate::{config, dirs, env, file};
use clap::ValueHint;
use eyre::Result;
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.
///
/// 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`.
///
/// Trust is shared across git worktrees: a config file inside a linked
/// worktree is trusted when the equivalent path in the repository's main
/// checkout has been trusted. Paranoid mode disables this sharing since
/// worktrees can check out branches with different config contents.
#[derive(Debug, clap::Args)]
#[clap(verbatim_doc_comment, after_long_help = AFTER_LONG_HELP)]
pub struct Trust {
/// The config file to trust
#[clap(value_hint = ValueHint::FilePath, verbatim_doc_comment)]
config_file: Option<PathBuf>,
/// Trust all config files in the current directory, its parents, and its subdirectories
///
/// Subdirectories are walked respecting .gitignore, skipping hidden directories
/// and common build/dependency directories (node_modules, vendor, target, dist, build).
#[clap(long, short, verbatim_doc_comment, conflicts_with_all = &["ignore", "untrust"])]
all: bool,
/// Do not trust this config and ignore it in the future
#[clap(long, conflicts_with = "untrust")]
ignore: bool,
/// Show the trusted status of config files from the current directory and its parents.
/// Does not trust or untrust any files.
#[clap(long, verbatim_doc_comment)]
show: bool,
/// No longer trust this config, will prompt in the future
#[clap(long)]
untrust: bool,
}
impl Trust {
pub async fn run(mut self) -> Result<()> {
if self.show {
return self.show();
}
if self.untrust {
untrust_config_file(self.config_file())
} else if self.ignore {
self.ignore()
} else if self.all {
while let Some(p) = self.get_next_untrusted() {
self.config_file = Some(p);
self.trust()?;
}
for p in self.get_untrusted_descendants() {
self.config_file = Some(p);
self.trust()?;
}
Ok(())
} else {
self.trust()
}
}
pub fn clean() -> Result<()> {
if dirs::TRUSTED_CONFIGS.is_dir() {
for path in file::ls(&dirs::TRUSTED_CONFIGS)? {
if !path.exists() {
remove_file(&path)?;
}
}
}
if dirs::IGNORED_CONFIGS.is_dir() {
for path in file::ls(&dirs::IGNORED_CONFIGS)? {
if !path.exists() {
remove_file(&path)?;
}
}
}
Ok(())
}
}
pub(super) fn untrust_config_file(config_file: Option<PathBuf>) -> Result<()> {
let path = match config_file {
Some(filename) => filename,
None => match ALL_CONFIG_FILES.first().cloned() {
Some(path) => path,
None => {
warn!("No trusted config files found.");
return Ok(());
}
},
};
let cfr = config_trust_root(&path);
config_file::untrust(&cfr)?;
let cfr = cfr.canonicalize()?;
info!("untrusted {}", cfr.display());
let trusted_via_settings = Settings::get()
.trusted_config_paths()
.any(|p| cfr.starts_with(p));
if trusted_via_settings {
warn!("{cfr:?} is trusted via settings so it will still be trusted.");
}
if !Settings::get().paranoid
&& let Some(main_path) = crate::git::main_checkout_equivalent(&cfr)
&& config_file::is_trusted(&main_path)
{
warn!(
"{} is a git worktree of {} which is trusted, so it will still be trusted. Untrust that path or use `mise trust --ignore`.",
display_path(&cfr),
display_path(&main_path)
);
}
Ok(())
}
pub(super) fn resolve_config_file(config_file: Option<&PathBuf>) -> Option<PathBuf> {
config_file.map(|config_file| {
if config_file.is_dir() {
config_files_in_dir(config_file)
.last()
.cloned()
.unwrap_or(config_file.join(&*env::MISE_DEFAULT_CONFIG_FILENAME))
} else {
config_file.clone()
}
})
}
impl Trust {
fn ignore(&self) -> Result<()> {
let path = match self.config_file() {
Some(filename) => filename,
None => match self.get_next() {
Some(path) => path,
None => {
warn!("No trusted config files found.");
return Ok(());
}
},
};
let cfr = config_trust_root(&path);
config_file::add_ignored(cfr.clone())?;
let cfr = cfr.canonicalize()?;
info!("ignored {}", cfr.display());
let trusted_via_settings = Settings::get()
.trusted_config_paths()
.any(|p| cfr.starts_with(p));
if trusted_via_settings {
warn!("{cfr:?} is trusted via settings so it will still be trusted.");
}
Ok(())
}
fn trust(&self) -> Result<()> {
let path = match self.config_file() {
Some(filename) => config_trust_root(&filename),
None => match self.get_next_untrusted() {
Some(path) => path,
None => {
warn!("No untrusted config files found.");
return Ok(());
}
},
};
config_file::trust(&path)?;
let cfr = path.canonicalize()?;
info!("trusted {}", cfr.display());
Ok(())
}
fn config_file(&self) -> Option<PathBuf> {
resolve_config_file(self.config_file.as_ref())
}
fn get_next(&self) -> Option<PathBuf> {
ALL_CONFIG_FILES.first().cloned()
}
fn get_next_untrusted(&self) -> Option<PathBuf> {
config::load_config_paths(&DEFAULT_CONFIG_FILENAMES, true)
.into_iter()
.filter(|p| !is_global_config(p))
.map(|p| config_trust_root(&p))
.unique()
.find(|ctr| !config_file::is_trusted(ctr))
}
/// Untrusted config files in subdirectories of the current directory.
///
/// Walks respecting .gitignore, skipping hidden directories and common
/// build/dependency directories so e.g. vendored configs in node_modules
/// or vendor are not trusted. Returns one config file per untrusted trust
/// root; `trust()` computes the trust root from each.
fn get_untrusted_descendants(&self) -> Vec<PathBuf> {
const EXCLUDED_DIRS: &[&str] = &["node_modules", "vendor", "target", "dist", "build"];
// Respect config discovery being disabled, matching load_config_paths
// used by the ancestor-walk pass.
if Settings::no_config() {
return vec![];
}
// Use the live cwd (not the cached dirs::CWD) so this anchors to the
// same directory as the ancestor-walk pass, which uses env::current_dir
// via load_config_paths -> all_dirs. A `cd` setting applied during
// settings load can move the process directory, and both passes must
// agree on where "here" is.
let Ok(cwd) = env::current_dir() else {
return vec![];
};
let walker = ignore::WalkBuilder::new(&cwd)
.hidden(true) // Skip hidden files/dirs
.git_ignore(true) // Respect .gitignore
.git_global(true) // Respect global .gitignore
.git_exclude(true) // Respect .git/info/exclude
.require_git(false) // Don't require a git repo
.filter_entry(|e| {
// Never exclude the walk root itself (depth 0), even if cwd is
// named e.g. `build` or `vendor` — otherwise nothing is walked.
if e.depth() == 0 {
return true;
}
let name = e.file_name().to_string_lossy();
!EXCLUDED_DIRS.contains(&name.as_ref())
})
.build();
let mut config_files = vec![];
for entry in walker {
let entry = match entry {
Ok(e) => e,
Err(err) => {
// Skip unreadable paths (permission denied, broken symlinks,
// etc.) so one bad directory doesn't abort the whole scan.
warn!("trust --all: skipping unreadable path: {err}");
continue;
}
};
if !entry.file_type().is_some_and(|ft| ft.is_dir()) {
continue;
}
let dir = entry.path();
if dir == cwd {
continue; // already covered by the parent walk
}
for p in config::config_paths_in_dir(dir) {
if !is_global_config(&p) {
config_files.push(p);
}
}
}
// Keep one config file per untrusted trust root.
let mut seen = HashSet::new();
config_files
.into_iter()
.filter(|p| {
let ctr = config_trust_root(p);
!config_file::is_trusted(&ctr) && seen.insert(ctr)
})
.collect()
}
fn show(&self) -> Result<()> {
let trusted = config::load_config_paths(&DEFAULT_CONFIG_FILENAMES, true)
.into_iter()
.filter(|p| !is_global_config(p))
.map(|p| config_trust_root(&p))
.unique()
.map(|p| (display_path(&p), config_file::is_trusted(&p)))
.rev()
.collect::<Vec<_>>();
if trusted.is_empty() {
info!("No trusted config files found.");
}
for (dp, trusted) in trusted {
if trusted {
miseprintln!("{dp}: trusted");
} else {
miseprintln!("{dp}: untrusted");
}
}
Ok(())
}
}
static AFTER_LONG_HELP: &str = color_print::cstr!(
r#"<bold><underline>Examples:</underline></bold>
# trusts ~/some_dir/mise.toml
$ <bold>mise trust ~/some_dir/mise.toml</bold>
# trusts mise.toml in the current or parent directory
$ <bold>mise trust</bold>
"#
);