Skip to content

fix(exec): propagate __MISE_DIFF so nested mise recovers pristine PATH#9765

Merged
jdx merged 4 commits into
mainfrom
fix/nested-exec-mise-diff
May 10, 2026
Merged

fix(exec): propagate __MISE_DIFF so nested mise recovers pristine PATH#9765
jdx merged 4 commits into
mainfrom
fix/nested-exec-mise-diff

Conversation

@jdx
Copy link
Copy Markdown
Owner

@jdx jdx commented May 10, 2026

Summary

A nested mise -C <new> exec -- <cmd> invoked from inside a mise run task or mise exec child process was inheriting the outer toolset's install paths as user-pre-PATH and outranking the inner toolset's resolved tool. mise which saw the right answer because it reads the toolset directly; mise exec/mise env build PATH on top of env::PATH and stacked the inner tool dirs after the outer ones.

The bug (from #9754)

With shims in PATH (the activated-shell case the discussion was filed against):

Outer task PATH Inner mise -C new exec PATH command -v finds
Before usage/3.0.0:…:shims:… usage/3.0.0:usage/3.2.1:shims:… usage/3.0.0
After usage/3.0.0:…:shims:… usage/3.2.1:shims:… (3.0.0 reverted) usage/3.2.1

PathEnv::from_iter classified the outer usage/3.0.0 as user-pre-PATH (everything before shims), so the inner toolset's usage/3.2.1, added to the mise slot, ended up behind it.

Fix

mise exec and mise run now embed __MISE_DIFF in the env they hand to children — the same mechanism mise hook-env already uses for activated shells. Nested mise reads it via the existing get_pristine_env machinery, reverses it, and builds its PATH from the pristine baseline. No new PATH zone, no path reclassification — just plumbing the existing env-diff mechanism into the spawn boundary it had been missing from.

The reusable bit (EnvDiff::from_final_env) extracts what hook-env was already doing inline.

Alternatives considered

The author of #9754 also opened #9760, which fixes the same bug at the PathEnv layer by taking inherited mise-install paths out of the pre segment, then re-appending them after the new tool paths but still before shims as a "demoted fallback." That works but adds a third PATH zone and ~220 lines of new platform-specific path-prefix logic. This PR is +47/-0 and reuses the env-diff plumbing that's already production-tested.

A noticeable behavior difference: this PR removes the outer task's tool dir from the inner's PATH entirely instead of keeping it as a fallback. I think that's the right semantics — mise -C <new> exec advertises "the env for <new>," not "merge of outer+inner." If a user wants both, they'd merge configs. (Though if the project disagrees, #9760's fallback approach can be layered on top.)

Test plan

  • cargo build --bin mise
  • mise run lint
  • mise run test:unit — 888 passed
  • mise run test:e2e cli/test_run_nested_exec_path — new regression test
  • Existing exec/run e2e tests pass: test_exec_chdir, test_exec_latest, test_exec_lockfile, test_exec_shim_recursion, test_exec_venv_path_order, test_exec_wrapper_recursion, test_exec_wrapper_recursion_with_shims
  • Existing activate/hook/env e2e tests pass: test_activate_export, test_activate_aggressive, test_hook_env, test_hook_env_dir_mtime_stabilizes, test_hook_env_fast_path, test_run_subtask_jobs1, test_set_env, test_use_env
  • Manually reproduced the discussion's scenario; confirmed the bug appears without the fix and is resolved with the fix
  • Edge cases verified: non-mise pre-PATH entries are preserved, no-shims PATH still works, tools not declared in either config remain findable via inherited PATH

Closes #9754

🤖 Generated with Claude Code


Note

Medium Risk
Changes how mise exec and mise run tasks construct child-process environments by adding __MISE_DIFF, which can affect PATH resolution in nested invocations. Risk is moderate because it alters tool lookup precedence and could change behavior for workflows that implicitly relied on outer tool paths leaking into inner commands.

Overview
Fixes nested mise -C <dir> exec invoked from within mise exec or mise run tasks so it no longer inherits and prioritizes the outer toolset’s install directories on PATH.

This propagates a serialized __MISE_DIFF to child environments (in src/cli/exec.rs and src/task/task_executor.rs) and adds EnvDiff::from_final_env to reliably compute that diff while excluding PATH and __MISE_DIFF itself. Adds an e2e regression test (e2e/cli/test_run_nested_exec_path) plus unit coverage for from_final_env to prevent PATH-leak regressions.

Reviewed by Cursor Bugbot for commit 7f4b3f8. Bugbot is set up for automated code reviews on this repo. Configure here.

A nested `mise -C <new> exec -- <cmd>` invoked from inside a `mise run`
task or `mise exec` child process was inheriting the outer toolset's
install paths as user-pre-PATH and outranking the inner toolset's
resolved tool. `mise which` saw the right answer because it reads the
toolset directly; `mise exec`/`mise env` build PATH on top of `env::PATH`
and stacked the inner tool dirs after the outer ones.

Fix: have `mise exec` and `mise run` embed `__MISE_DIFF` in the env they
hand to children, exactly the way `mise hook-env` does for activated
shells. Nested mise then reverses it via the existing `get_pristine_env`
machinery and builds its PATH from the pristine baseline. No new PATH
zone, no path reclassification — just plumbing the existing env-diff
mechanism into the spawn boundary it had been missing from.

Closes #9754

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Copy link
Copy Markdown
Contributor

@gemini-code-assist gemini-code-assist Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request addresses an issue where nested mise executions would incorrectly prioritize the outer toolset's paths over the inner one's. It introduces a mechanism to embed __MISE_DIFF into the environment of child processes in both exec and task execution, enabling nested instances to restore the pristine environment. Feedback suggests optimizing the EnvDiff::from_final_env method by using iterators to avoid unnecessary cloning and excluding __MISE_DIFF from the diff to prevent recursive nesting in deep process hierarchies.

Comment thread src/env_diff.rs Outdated
Comment on lines +180 to +182
let mut additions = final_env.clone();
additions.remove(PATH_KEY.as_str());
let mut diff = EnvDiff::new(pristine, additions);
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

To avoid recursive nesting of the __MISE_DIFF environment variable in deep sub-process hierarchies, it should be excluded from the diff calculation. Additionally, using an iterator instead of cloning the entire environment map is more efficient.

Suggested change
let mut additions = final_env.clone();
additions.remove(PATH_KEY.as_str());
let mut diff = EnvDiff::new(pristine, additions);
let additions = final_env.iter()
.filter(|(k, _)| k.as_str() != PATH_KEY.as_str() && k.as_str() != "__MISE_DIFF")
.map(|(k, v)| (k.clone(), v.clone()));
let mut diff = EnvDiff::new(pristine, additions);

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good call on both, applied in c9dcdad. The __MISE_DIFF filter is defensive — env_with_path and full_env shouldn't put __MISE_DIFF into the additions today, but explicitly skipping it means a future code path that does won't accidentally nest the diff inside itself.

This reply was generated by an AI coding assistant.

@greptile-apps
Copy link
Copy Markdown
Contributor

greptile-apps Bot commented May 10, 2026

Greptile Summary

This PR fixes a PATH-stacking bug where a nested mise -C <dir> exec invocation inside a mise run task or mise exec child process would inherit the outer toolset's install dirs as user-pre-PATH, causing the outer tool version to outrank the inner toolset's resolved tool.

  • EnvDiff::from_final_env is extracted as a reusable method (previously only done inline in hook-env) to compute the diff from pristine to the current toolset env; it correctly excludes PATH (tracked separately via diff.path) and __MISE_DIFF itself.
  • mise exec and task execution now serialize the resulting diff into __MISE_DIFF before spawning the child process, mirroring what shell activation already does so that nested mise can call get_pristine_env and reverse the outer toolset's PATH changes before resolving its own tools.
  • A comprehensive unit test (test_from_final_env) and an e2e regression test (test_run_nested_exec_path) cover the fix end-to-end, including the shims-in-PATH scenario that originally triggered the bug.

Confidence Score: 5/5

Safe to merge; the change is narrowly scoped to injecting __MISE_DIFF at the exec boundary and reuses the existing, production-tested pristine-env reversal machinery.

The fix threads existing EnvDiff plumbing through two call sites (exec.rs and task_executor.rs) that were previously missing it. from_final_env correctly excludes PATH from the key/value maps and tracks it separately via set-difference, matching exactly what get_pristine_env expects. The new unit test pins all four key behaviors, and the e2e test reproduces the original shims-in-PATH scenario. No existing PATH handling is changed; the fallback when serialize() fails is the prior behavior rather than a crash. No removal of pristine vars can be silently missed because env_with_path is additive over PRISTINE_ENV.

No files require special attention.

Important Files Changed

Filename Overview
src/env_diff.rs Adds from_final_env to compute the pristine→final diff for __MISE_DIFF injection; PATH tracked via set-difference, __MISE_DIFF excluded from additions. New unit test covers all four advertised behaviors plus round-trip reversal.
src/cli/exec.rs Injects serialized __MISE_DIFF into the child env after all other env modifications and before the sandbox filter; correctly mirrors task_executor.rs.
src/task/task_executor.rs Same __MISE_DIFF injection pattern as exec.rs; positioned after all env mutations including the env-cache key, so the serialized diff is complete.
e2e/cli/test_run_nested_exec_path New e2e regression test that reproduces the shims-as-separator scenario from discussion #9754 and asserts the inner toolset's binary is found by the nested exec.

Reviews (4): Last reviewed commit: "fix(exec): compute __MISE_DIFF after all..." | Re-trigger Greptile

Comment thread src/env_diff.rs
- Filter __MISE_DIFF out of from_final_env's additions so an inherited
  value can't end up nested inside the diff being written. Previously
  inert (the iterator skip is defensive in case a future code path puts
  __MISE_DIFF into the env we hand to from_final_env), but cleaner.
- Build additions via iterator instead of cloning the whole map and
  removing one entry.
- Add unit test pinning the key behaviors of from_final_env: shared PATH
  entries excluded from diff.path, only-in-final entries included,
  non-PATH adds/changes tracked in old/new, PATH/__MISE_DIFF filtered
  from old/new, and round-trip via reverse + path strip restores
  pristine.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Copy link
Copy Markdown

@cursor cursor Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes and found 1 potential issue.

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit c9dcdad. Configure here.

Comment thread src/cli/exec.rs Outdated
Test was hardcoding ":" as the PATH separator, which is wrong on Windows
(uses ";"). split_paths(final_path) treated the whole hardcoded value as
a single entry on Windows, so the assertion comparing diff.path to
[/tool/bin] saw [/tool/bin:/usr/bin:/bin] instead. Build the PATH string
via std::env::join_paths and compare back via split_paths so the test is
portable.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@github-actions
Copy link
Copy Markdown

github-actions Bot commented May 10, 2026

Hyperfine Performance

mise x -- echo

Command Mean [ms] Min [ms] Max [ms] Relative
mise-2026.5.5 x -- echo 20.4 ± 0.8 18.8 23.6 1.00
mise x -- echo 20.9 ± 1.2 19.2 37.8 1.02 ± 0.07

mise env

Command Mean [ms] Min [ms] Max [ms] Relative
mise-2026.5.5 env 20.0 ± 0.9 17.8 24.0 1.00
mise env 20.2 ± 0.8 18.7 24.9 1.01 ± 0.06

mise hook-env

Command Mean [ms] Min [ms] Max [ms] Relative
mise-2026.5.5 hook-env 21.1 ± 0.9 19.6 26.7 1.00
mise hook-env 21.3 ± 0.9 19.6 25.1 1.01 ± 0.06

mise ls

Command Mean [ms] Min [ms] Max [ms] Relative
mise-2026.5.5 ls 17.4 ± 0.8 15.7 21.2 1.00
mise ls 17.6 ± 0.8 16.1 21.9 1.01 ± 0.07

xtasks/test/perf

Command mise-2026.5.5 mise Variance
install (cached) 128ms 128ms +0%
ls (cached) 59ms 60ms -1%
bin-paths (cached) 63ms 64ms -1%
task-ls (cached) 499ms 504ms +0%

Matches the ordering in task_executor.rs so the diff fully describes what
mise added to the env, including MISE_ENV and __MISE_ENV_CACHE_KEY. No
behavior change for the PATH fix; future callers relying on get_pristine_env
to reverse those vars will now get a consistent view across both spawn
boundaries.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@jdx jdx merged commit 200b10e into main May 10, 2026
35 checks passed
@jdx jdx deleted the fix/nested-exec-mise-diff branch May 10, 2026 13:37
mise-en-dev added a commit that referenced this pull request May 11, 2026
### 🚀 Features

- **(cli)** add minimum release age flag to lock and ls-remote by
@risu729 in [#9269](#9269)
- **(config)** add run field for hooks by @risu729 in
[#9718](#9718)
- **(github)** add native oauth token source by @jdx in
[#9654](#9654)
- **(oci)** scope build to project config by default by @jdx in
[#9766](#9766)
- add support for prefixed latest version queries in outdated checks by
@roele in [#9767](#9767)

### 🐛 Bug Fixes

- **(activate)** guard bash chpwd hook under nounset by @risu729 in
[#9716](#9716)
- **(backend)** date-check latest stable fast path by @risu729 in
[#9650](#9650)
- **(config)** parse core tool options consistently by @risu729 in
[#9742](#9742)
- **(exec)** propagate __MISE_DIFF so nested mise recovers pristine PATH
by @jdx in [#9765](#9765)
- **(forgejo)** include prereleases when opted in by @risu729 in
[#9717](#9717)
- **(github)** avoid caching empty release assets by @risu729 in
[#9616](#9616)
- **(java)** resolve lockfile URLs from metadata by @risu729 in
[#9719](#9719)
- **(lock)** cache unavailable github attestations by @risu729 in
[#9741](#9741)
- **(pipx)** preserve options when reinstalling tools by @risu729 in
[#9663](#9663)
- **(python)** skip redundant lockfile provenance verification by
@risu729 in [#9739](#9739)
- **(vfox)** run pre_uninstall hook by @risu729 in
[#9662](#9662)

### 🚜 Refactor

- **(schema)** extract tool options definition by @risu729 in
[#9649](#9649)

### ⚡ Performance

- **(aqua)** bake rkyv aqua package blobs by @risu729 in
[#9535](#9535)

### 📦️ Dependency Updates

- lock file maintenance by @renovate[bot] in
[#9773](#9773)

### 📦 Registry

- add vector
([github:vectordotdev/vector](https://github.com/vectordotdev/vector))
by @kquinsland in [#9761](#9761)
- add oc and openshift-install (http backend) by @konono in
[#9669](#9669)

### New Contributors

- @konono made their first contribution in
[#9669](#9669)
- @kquinsland made their first contribution in
[#9761](#9761)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant