fix(exec): propagate __MISE_DIFF so nested mise recovers pristine PATH#9765
Conversation
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>
There was a problem hiding this comment.
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.
| let mut additions = final_env.clone(); | ||
| additions.remove(PATH_KEY.as_str()); | ||
| let mut diff = EnvDiff::new(pristine, additions); |
There was a problem hiding this comment.
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.
| 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); |
There was a problem hiding this comment.
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 SummaryThis PR fixes a PATH-stacking bug where a nested
Confidence Score: 5/5Safe to merge; the change is narrowly scoped to injecting The fix threads existing No files require special attention. Important Files Changed
Reviews (4): Last reviewed commit: "fix(exec): compute __MISE_DIFF after all..." | Re-trigger Greptile |
- 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>
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 1 potential issue.
❌ 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.
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>
Hyperfine Performance
|
| 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>
### 🚀 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)

Summary
A nested
mise -C <new> exec -- <cmd>invoked from inside amise runtask ormise execchild process was inheriting the outer toolset's install paths as user-pre-PATH and outranking the inner toolset's resolved tool.mise whichsaw the right answer because it reads the toolset directly;mise exec/mise envbuild PATH on top ofenv::PATHand 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):
mise -C new execPATHcommand -vfindsusage/3.0.0:…:shims:…usage/3.0.0:usage/3.2.1:shims:…usage/3.0.0❌usage/3.0.0:…:shims:…usage/3.2.1:shims:…(3.0.0 reverted)usage/3.2.1✓PathEnv::from_iterclassified the outerusage/3.0.0as user-pre-PATH (everything before shims), so the inner toolset'susage/3.2.1, added to themiseslot, ended up behind it.Fix
mise execandmise runnow embed__MISE_DIFFin the env they hand to children — the same mechanismmise hook-envalready uses for activated shells. Nested mise reads it via the existingget_pristine_envmachinery, 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 whathook-envwas already doing inline.Alternatives considered
The author of #9754 also opened #9760, which fixes the same bug at the
PathEnvlayer by taking inherited mise-install paths out of thepresegment, 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> execadvertises "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 misemise run lintmise run test:unit— 888 passedmise run test:e2e cli/test_run_nested_exec_path— new regression testtest_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_shimstest_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_envCloses #9754
🤖 Generated with Claude Code
Note
Medium Risk
Changes how
mise execandmise runtasks 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> execinvoked from withinmise execormise runtasks so it no longer inherits and prioritizes the outer toolset’s install directories onPATH.This propagates a serialized
__MISE_DIFFto child environments (insrc/cli/exec.rsandsrc/task/task_executor.rs) and addsEnvDiff::from_final_envto reliably compute that diff while excludingPATHand__MISE_DIFFitself. Adds an e2e regression test (e2e/cli/test_run_nested_exec_path) plus unit coverage forfrom_final_envto prevent PATH-leak regressions.Reviewed by Cursor Bugbot for commit 7f4b3f8. Bugbot is set up for automated code reviews on this repo. Configure here.