fix(exec): strip shims from PATH on Unix to prevent infinite recursion#8276
fix(exec): strip shims from PATH on Unix to prevent infinite recursion#8276
Conversation
Summary of ChangesHello @jdx, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed! This pull request resolves a critical infinite recursion bug affecting Highlights
Changelog
Activity
Using Gemini Code AssistThe full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips. Invoking Gemini You can request assistance from Gemini at any point by creating a comment using either
Customization To customize Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a Limitations & Feedback Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counter productive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for Github and other Google products, sign up here. You can also get AI-powered code generation, chat, as well as code reviews directly in the IDE at no cost with the Gemini Code Assist IDE Extension. Footnotes
|
There was a problem hiding this comment.
Code Review
The pull request correctly addresses the infinite recursion issue on Unix by filtering the shims directory from PATH during program resolution. This brings the Unix implementation in line with the existing Windows fix. I've suggested a few refinements to the resolution logic to improve robustness and ensure consistency with the Windows implementation.
| let program = if program.to_string_lossy().contains('/') { | ||
| // Already a path, no need to resolve | ||
| program | ||
| } else { | ||
| let cwd = crate::dirs::CWD.clone().unwrap_or_default(); | ||
| let lookup_path = env.get(&*env::PATH_KEY).map(|path_val| { | ||
| let shims_dir = &*crate::dirs::SHIMS; | ||
| let filtered: Vec<_> = std::env::split_paths(&OsString::from(path_val)) | ||
| .filter(|p| p != shims_dir) | ||
| .collect(); | ||
| std::env::join_paths(&filtered).unwrap() | ||
| }); | ||
| match which::which_in(&program, lookup_path, cwd) { | ||
| Ok(resolved) => resolved.into_os_string(), | ||
| Err(_) => program, // Fall back to original if resolution fails | ||
| } | ||
| }; |
There was a problem hiding this comment.
The program resolution logic for Unix can be improved for robustness and consistency with the Windows implementation.
- Path Expansion: Using
crate::file::replace_path(p)when filteringPATHensures that entries containing~are correctly handled, which is important for robustness and matches the Windows implementation. - Error Handling: Removing the fallback to the original
programname when resolution fails is safer. Ifwhich_incannot find the executable outside of the shims directory, falling back to the original name will likely causeexecvpto find the shim again, leading back to the infinite recursion. Using?to propagate the error is consistent with the Windows implementation and provides a clear 'command not found' error instead of potentially entering a loop.
let program = if program.to_string_lossy().contains('/') {
// Already a path, no need to resolve
program
} else {
let cwd = crate::dirs::CWD.clone().unwrap_or_default();
let lookup_path = env.get(&*env::PATH_KEY).map(|path_val| {
let shims_dir = &*crate::dirs::SHIMS;
let filtered: Vec<_> = std::env::split_paths(&OsString::from(path_val))
.filter(|p| crate::file::replace_path(p) != shims_dir)
.collect();
std::env::join_paths(&filtered).unwrap()
});
which::which_in(&program, lookup_path, cwd)?.into_os_string()
};There was a problem hiding this comment.
Pull request overview
This PR fixes an infinite recursion bug on Unix systems where mise x -- tool can enter an infinite loop when not_found_auto_install preserves the shims directory in PATH. The fix mirrors the existing Windows implementation by filtering the shims directory from PATH during program resolution while still passing the full PATH to the child process.
Changes:
- Apply shims directory filtering to Unix
exec_programfunction (matching Windows fix from commit 3ec95cf) - Add Unix e2e regression test
test_exec_shim_recursionto verify shims are not recursively executed
Reviewed changes
Copilot reviewed 2 out of 2 changed files in this pull request and generated 3 comments.
| File | Description |
|---|---|
| src/cli/exec.rs | Adds shim directory filtering logic to Unix exec_program function to prevent infinite recursion when resolving program paths |
| e2e/cli/test_exec_shim_recursion | Adds regression test that verifies mise exec resolves real tool binaries instead of shims when shims appear first in PATH |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| let shims_dir = &*crate::dirs::SHIMS; | ||
| let filtered: Vec<_> = std::env::split_paths(&OsString::from(path_val)) | ||
| .filter(|p| p != shims_dir) |
There was a problem hiding this comment.
The path comparison here does not expand tilde (~) paths, which could cause the shims directory to not be filtered correctly if it's specified with a tilde. The Windows implementation (lines 247-248) uses crate::file::replace_path(p) to expand tilde before comparison. Consider adding the same expansion here for consistency and correctness.
| let shims_dir = &*crate::dirs::SHIMS; | |
| let filtered: Vec<_> = std::env::split_paths(&OsString::from(path_val)) | |
| .filter(|p| p != shims_dir) | |
| let shims_dir = crate::file::replace_path(&*crate::dirs::SHIMS); | |
| let filtered: Vec<_> = std::env::split_paths(&OsString::from(path_val)) | |
| .filter(|p| crate::file::replace_path(p) != shims_dir) |
| match which::which_in(&program, lookup_path, cwd) { | ||
| Ok(resolved) => resolved.into_os_string(), | ||
| Err(_) => program, // Fall back to original if resolution fails | ||
| } |
There was a problem hiding this comment.
This fallback behavior differs from the Windows implementation (line 253), which propagates the error using the ? operator. Falling back to the original program could mask issues where the tool doesn't exist at all, making it harder to debug. Consider whether this should propagate the error like Windows does, or if there's a specific reason for the different behavior that should be documented in a comment.
| match which::which_in(&program, lookup_path, cwd) { | |
| Ok(resolved) => resolved.into_os_string(), | |
| Err(_) => program, // Fall back to original if resolution fails | |
| } | |
| which::which_in(&program, lookup_path, cwd)?.into_os_string() |
| let filtered: Vec<_> = std::env::split_paths(&OsString::from(path_val)) | ||
| .filter(|p| p != shims_dir) | ||
| .collect(); | ||
| std::env::join_paths(&filtered).unwrap() |
There was a problem hiding this comment.
This unwrap() could panic if join_paths fails (e.g., if a path contains a null byte on Unix). The Windows implementation (line 251) also uses unwrap() here, but it would be more robust to handle this error case, perhaps by propagating the error or logging a warning and falling back to the original PATH.
| std::env::join_paths(&filtered).unwrap() | |
| match std::env::join_paths(&filtered) { | |
| Ok(joined) => joined, | |
| Err(err) => { | |
| eprintln!( | |
| "warning: failed to rebuild PATH without shims ({}); using original PATH", | |
| err | |
| ); | |
| OsString::from(path_val) | |
| } | |
| } |
Hyperfine Performance
|
| Command | Mean [ms] | Min [ms] | Max [ms] | Relative |
|---|---|---|---|---|
mise-2026.2.17 x -- echo |
21.9 ± 0.2 | 21.4 | 24.2 | 1.00 |
mise x -- echo |
22.8 ± 0.2 | 22.3 | 25.5 | 1.04 ± 0.02 |
mise env
| Command | Mean [ms] | Min [ms] | Max [ms] | Relative |
|---|---|---|---|---|
mise-2026.2.17 env |
21.5 ± 0.6 | 20.9 | 27.8 | 1.00 |
mise env |
21.6 ± 0.2 | 20.7 | 23.4 | 1.00 ± 0.03 |
mise hook-env
| Command | Mean [ms] | Min [ms] | Max [ms] | Relative |
|---|---|---|---|---|
mise-2026.2.17 hook-env |
22.0 ± 0.2 | 21.0 | 24.9 | 1.00 |
mise hook-env |
22.2 ± 0.2 | 21.7 | 22.9 | 1.01 ± 0.01 |
mise ls
| Command | Mean [ms] | Min [ms] | Max [ms] | Relative |
|---|---|---|---|---|
mise-2026.2.17 ls |
19.6 ± 0.2 | 18.9 | 21.4 | 1.00 |
mise ls |
19.9 ± 0.3 | 19.4 | 22.2 | 1.01 ± 0.02 |
xtasks/test/perf
| Command | mise-2026.2.17 | mise | Variance |
|---|---|---|---|
| install (cached) | 121ms | 121ms | +0% |
| ls (cached) | 74ms | 74ms | +0% |
| bin-paths (cached) | 78ms | 78ms | +0% |
| task-ls (cached) | 787ms | 793ms | +0% |
When `not_found_auto_install` preserves the shims directory in PATH, `mise x -- tool` can enter an infinite loop: execvp finds a wrapper script (or shim) instead of the real binary, the wrapper calls `mise x -- tool` again, and PATH grows until hitting E2BIG. Apply the same fix that Windows already has (3ec95cf): filter the shims directory from PATH when resolving the program via which::which_in, while still passing the full PATH (with shims) to the child process. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The shim-stripping fix prevents recursion through shims, but wrapper scripts in non-shim directories (e.g., .devcontainer/bin/tool) can still cause infinite loops. These wrappers call `mise x -- tool`, and since .devcontainer/bin precedes mise install paths in PATH, execvp finds the wrapper again. Fix by resolving the program via ts.which_bin() before exec. If the program is provided by a mise-managed tool, use the full install path, bypassing any wrappers or shims in PATH. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
683b3f9 to
f75c105
Compare
The which_bin pre-resolution converts a bare command name (e.g. "prettier") to its full installed path before calling exec_program. On Windows, this bypasses the which::which_in PATHEXT resolution that correctly finds .cmd/.exe wrappers, causing "not a valid Win32 application" (OS error 193) when the pre-resolved .cmd path is passed directly to duct::cmd. Make the which_bin resolution #[cfg(unix)] only since the underlying problem (execvp wrapper recursion) is Unix-specific. On Windows, exec_program's which::which_in with shim-filtered PATH already handles the recursion case correctly. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
f75c105 to
d53b221
Compare
### 🚀 Features - **(install)** auto-lock all platforms after tool installation by @jdx in [#8277](#8277) ### 🐛 Bug Fixes - **(config)** respect --yes flag for config trust prompts by @jdx in [#8288](#8288) - **(exec)** strip shims from PATH on Unix to prevent infinite recursion by @jdx in [#8276](#8276) - **(install)** validate --locked before --dry-run short-circuit by @altendky in [#8290](#8290) - **(release)** refresh PATH after mise up in release-plz by @jdx in [#8292](#8292) - **(schema)** replace unevaluatedProperties with additionalProperties by @jdx in [#8285](#8285) - **(task)** avoid duplicated stderr on task failure in replacing mode by @jdx in [#8275](#8275) - **(task)** use process groups to kill child process trees on Unix by @jdx in [#8279](#8279) - **(task)** run depends_post tasks even when parent task fails by @jdx in [#8274](#8274) - **(task)** suggest similar commands when mistyping a CLI subcommand by @jdx in [#8286](#8286) - **(task)** execute monorepo subdirectory prepare steps from root by @jdx in [#8291](#8291) - **(upgrade)** don't force-reinstall already installed versions by @jdx in [#8282](#8282) - **(watch)** restore terminal state after watchexec exits by @jdx in [#8273](#8273) ### 📚 Documentation - clarify that MISE_CEILING_PATHS excludes the ceiling directory itself by @jdx in [#8283](#8283) ### Chore - replace gen-release-notes script with communique by @jdx in [#8289](#8289) ### New Contributors - @altendky made their first contribution in [#8290](#8290) ## 📦 Aqua Registry Updates #### New Packages (4) - [`Skarlso/crd-to-sample-yaml`](https://github.com/Skarlso/crd-to-sample-yaml) - [`kunobi-ninja/kunobi-releases`](https://github.com/kunobi-ninja/kunobi-releases) - [`swanysimon/markdownlint-rs`](https://github.com/swanysimon/markdownlint-rs) - [`tmux/tmux-builds`](https://github.com/tmux/tmux-builds) #### Updated Packages (2) - [`firecow/gitlab-ci-local`](https://github.com/firecow/gitlab-ci-local) - [`k1LoW/runn`](https://github.com/k1LoW/runn)
## [2026.2.18](https://github.com/jdx/mise/compare/v2026.2.17..v2026.2.18) - 2026-02-21 ### 🚀 Features - **(install)** auto-lock all platforms after tool installation by @jdx in [#8277](jdx/mise#8277) ### 🐛 Bug Fixes - **(config)** respect --yes flag for config trust prompts by @jdx in [#8288](jdx/mise#8288) - **(exec)** strip shims from PATH on Unix to prevent infinite recursion by @jdx in [#8276](jdx/mise#8276) - **(install)** validate --locked before --dry-run short-circuit by @altendky in [#8290](jdx/mise#8290) - **(release)** refresh PATH after mise up in release-plz by @jdx in [#8292](jdx/mise#8292) - **(schema)** replace unevaluatedProperties with additionalProperties by @jdx in [#8285](jdx/mise#8285) - **(task)** avoid duplicated stderr on task failure in replacing mode by @jdx in [#8275](jdx/mise#8275) - **(task)** use process groups to kill child process trees on Unix by @jdx in [#8279](jdx/mise#8279) - **(task)** run depends_post tasks even when parent task fails by @jdx in [#8274](jdx/mise#8274) - **(task)** suggest similar commands when mistyping a CLI subcommand by @jdx in [#8286](jdx/mise#8286) - **(task)** execute monorepo subdirectory prepare steps from root by @jdx in [#8291](jdx/mise#8291) - **(upgrade)** don't force-reinstall already installed versions by @jdx in [#8282](jdx/mise#8282) - **(watch)** restore terminal state after watchexec exits by @jdx in [#8273](jdx/mise#8273) ### 📚 Documentation - clarify that MISE_CEILING_PATHS excludes the ceiling directory itself by @jdx in [#8283](jdx/mise#8283) ### Chore - replace gen-release-notes script with communique by @jdx in [#8289](jdx/mise#8289) ### New Contributors - @altendky made their first contribution in [#8290](jdx/mise#8290) ### 📦 Aqua Registry Updates #### New Packages (4) - [`Skarlso/crd-to-sample-yaml`](https://github.com/Skarlso/crd-to-sample-yaml) - [`kunobi-ninja/kunobi-releases`](https://github.com/kunobi-ninja/kunobi-releases) - [`swanysimon/markdownlint-rs`](https://github.com/swanysimon/markdownlint-rs) - [`tmux/tmux-builds`](https://github.com/tmux/tmux-builds) #### Updated Packages (2) - [`firecow/gitlab-ci-local`](https://github.com/firecow/gitlab-ci-local) - [`k1LoW/runn`](https://github.com/k1LoW/runn) ## [2026.2.17](https://github.com/jdx/mise/compare/v2026.2.16..v2026.2.17) - 2026-02-19 ### 🚀 Features - **(prepare)** update mtime of outputs after command is run by @halms in [#8243](jdx/mise#8243) ### 🐛 Bug Fixes - **(install)** use backend bin paths for per-tool postinstall hooks by @jdx in [#8234](jdx/mise#8234) - **(use)** write to config.toml instead of config.local.toml by @jdx in [#8240](jdx/mise#8240) - default legacy .mise.backend installs to non-explicit by @jean-humann in [#8245](jdx/mise#8245) ### 🚜 Refactor - **(config)** consolidate flat task_* settings into nested task.* by @jdx in [#8239](jdx/mise#8239) ### Chore - **(prepare)** refactor common code into ProviderBase by @halms in [#8246](jdx/mise#8246) ### 📦 Aqua Registry Updates #### Updated Packages (1) - [`namespacelabs/foundation/nsc`](https://github.com/namespacelabs/foundation/nsc)
Remove the `which_bin` pre-resolution that was introduced in #8276 to prevent wrapper script recursion. This pre-resolution bypasses PATH entirely for mise-managed tools, breaking `_.python.venv` and similar configs where a virtualenv binary should take precedence over the mise-managed install. The shim stripping in `exec_program` (also from #8276) already prevents the main recursion vector (shims in PATH). The wrapper script edge case should not be solved by bypassing PATH for all programs. Fixes #8340 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Addresses Cursor Bugbot feedback on PR #8342. Simulates the .devcontainer/bin/tool scenario from #8276 where a wrapper script that calls `mise x -- tool` sits before mise tool paths in the system PATH. The test passes because `exec_program` resolves programs via the mise-computed PATH (where tool install paths are prepended before the original system PATH), so the real binary is found before the wrapper. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
) ## Summary - Remove the `ts.which_bin()` pre-resolution from `Exec::run()` that was introduced in #8276 - This pre-resolution bypassed PATH entirely for mise-managed tools, breaking `_.python.venv` and similar configs where a virtualenv binary should take precedence over the mise-managed install - The shim stripping in `exec_program` (also from #8276) already prevents shim recursion, which was the primary problem that PR solved ## Root Cause PR #8276 added two fixes for infinite recursion: 1. Strip shims from PATH in `exec_program` — prevents shim recursion (**kept**) 2. Pre-resolve bare program names via `which_bin` — prevents wrapper script recursion (**removed**) Fix #2 resolves e.g. `python` → `~/.local/share/mise/installs/python/3.9.1/bin/python` before PATH is computed. Since the result is an absolute path, `exec_program` skips PATH lookup entirely, so `.venv/bin/python` (added by `_.python.venv`) is never found. Fixes #8340 ## Test plan - [x] `cargo check` — no warnings - [x] `mise run test:e2e test_exec` — all exec e2e tests pass - [x] `test_exec_shim_recursion` still passes (shim stripping in `exec_program` handles this) 🤖 Generated with [Claude Code](https://claude.com/claude-code) <!-- CURSOR_SUMMARY --> --- > [!NOTE] > **Medium Risk** > Changes how `mise x` resolves executables, which can alter which binary runs in edge PATH setups. Added E2E tests reduce regression risk, but behavior changes could impact users relying on the old forced mise-path resolution. > > **Overview** > `mise x` no longer pre-resolves bare command names to a mise-managed tool path via `Toolset::which_bin` in `Exec::run`, allowing normal PATH lookup to pick up higher-precedence overrides like virtualenv binaries. > > Adds two E2E regression tests: one ensuring PATH-prepended “venv” bins win over mise installs during `mise x`, and another ensuring a PATH-preceding wrapper that re-invokes `mise x` doesn’t hang (infinite recursion) and still executes the real tool. > > <sup>Written by [Cursor Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit b241665. This will update automatically on new commits. Configure [here](https://cursor.com/dashboard?tab=bugbot).</sup> <!-- /CURSOR_SUMMARY --> --------- Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
jdx#8276) ## Summary - When `not_found_auto_install` preserves the shims directory in PATH, `mise x -- tool` can enter an infinite loop on Unix, eventually hitting `E2BIG` ("Argument list too long") - **Commit 1**: Strip shims from PATH during program resolution in Unix `exec_program` (parity with the existing Windows fix from `3ec95cf9a`). This prevents recursion through shims. - **Commit 2**: Resolve the program via `ts.which_bin()` in `Exec::run()` before exec. This prevents recursion through wrapper scripts (e.g., `.devcontainer/bin/tool`) that call `mise x -- tool` — the wrapper precedes mise install paths in PATH, so without resolution the wrapper is found again instead of the real binary. - Add a Unix e2e regression test (`test_exec_shim_recursion`) mirroring the existing Windows test ## Test plan - [x] `cargo check` passes - [x] `cargo test` — all 481 tests pass - [x] All pre-commit lints pass (shfmt, shellcheck, cargo-fmt, etc.) - [x] Tested on Coder workspace: `coder ssh jdx.devcontainer -- 'claude --version'` returns `2.1.50 (Claude Code)` immediately (previously hung in infinite recursion) - [ ] `./e2e/run_test cli/test_exec_shim_recursion` passes - [ ] Existing exec e2e tests still pass (`test_exec_chdir`, `test_exec_latest`, `test_shims_fallback`) 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
### 🚀 Features - **(install)** auto-lock all platforms after tool installation by @jdx in [jdx#8277](jdx#8277) ### 🐛 Bug Fixes - **(config)** respect --yes flag for config trust prompts by @jdx in [jdx#8288](jdx#8288) - **(exec)** strip shims from PATH on Unix to prevent infinite recursion by @jdx in [jdx#8276](jdx#8276) - **(install)** validate --locked before --dry-run short-circuit by @altendky in [jdx#8290](jdx#8290) - **(release)** refresh PATH after mise up in release-plz by @jdx in [jdx#8292](jdx#8292) - **(schema)** replace unevaluatedProperties with additionalProperties by @jdx in [jdx#8285](jdx#8285) - **(task)** avoid duplicated stderr on task failure in replacing mode by @jdx in [jdx#8275](jdx#8275) - **(task)** use process groups to kill child process trees on Unix by @jdx in [jdx#8279](jdx#8279) - **(task)** run depends_post tasks even when parent task fails by @jdx in [jdx#8274](jdx#8274) - **(task)** suggest similar commands when mistyping a CLI subcommand by @jdx in [jdx#8286](jdx#8286) - **(task)** execute monorepo subdirectory prepare steps from root by @jdx in [jdx#8291](jdx#8291) - **(upgrade)** don't force-reinstall already installed versions by @jdx in [jdx#8282](jdx#8282) - **(watch)** restore terminal state after watchexec exits by @jdx in [jdx#8273](jdx#8273) ### 📚 Documentation - clarify that MISE_CEILING_PATHS excludes the ceiling directory itself by @jdx in [jdx#8283](jdx#8283) ### Chore - replace gen-release-notes script with communique by @jdx in [jdx#8289](jdx#8289) ### New Contributors - @altendky made their first contribution in [jdx#8290](jdx#8290) ## 📦 Aqua Registry Updates #### New Packages (4) - [`Skarlso/crd-to-sample-yaml`](https://github.com/Skarlso/crd-to-sample-yaml) - [`kunobi-ninja/kunobi-releases`](https://github.com/kunobi-ninja/kunobi-releases) - [`swanysimon/markdownlint-rs`](https://github.com/swanysimon/markdownlint-rs) - [`tmux/tmux-builds`](https://github.com/tmux/tmux-builds) #### Updated Packages (2) - [`firecow/gitlab-ci-local`](https://github.com/firecow/gitlab-ci-local) - [`k1LoW/runn`](https://github.com/k1LoW/runn)
…x#8342) ## Summary - Remove the `ts.which_bin()` pre-resolution from `Exec::run()` that was introduced in jdx#8276 - This pre-resolution bypassed PATH entirely for mise-managed tools, breaking `_.python.venv` and similar configs where a virtualenv binary should take precedence over the mise-managed install - The shim stripping in `exec_program` (also from jdx#8276) already prevents shim recursion, which was the primary problem that PR solved ## Root Cause PR jdx#8276 added two fixes for infinite recursion: 1. Strip shims from PATH in `exec_program` — prevents shim recursion (**kept**) 2. Pre-resolve bare program names via `which_bin` — prevents wrapper script recursion (**removed**) Fix #2 resolves e.g. `python` → `~/.local/share/mise/installs/python/3.9.1/bin/python` before PATH is computed. Since the result is an absolute path, `exec_program` skips PATH lookup entirely, so `.venv/bin/python` (added by `_.python.venv`) is never found. Fixes jdx#8340 ## Test plan - [x] `cargo check` — no warnings - [x] `mise run test:e2e test_exec` — all exec e2e tests pass - [x] `test_exec_shim_recursion` still passes (shim stripping in `exec_program` handles this) 🤖 Generated with [Claude Code](https://claude.com/claude-code) <!-- CURSOR_SUMMARY --> --- > [!NOTE] > **Medium Risk** > Changes how `mise x` resolves executables, which can alter which binary runs in edge PATH setups. Added E2E tests reduce regression risk, but behavior changes could impact users relying on the old forced mise-path resolution. > > **Overview** > `mise x` no longer pre-resolves bare command names to a mise-managed tool path via `Toolset::which_bin` in `Exec::run`, allowing normal PATH lookup to pick up higher-precedence overrides like virtualenv binaries. > > Adds two E2E regression tests: one ensuring PATH-prepended “venv” bins win over mise installs during `mise x`, and another ensuring a PATH-preceding wrapper that re-invokes `mise x` doesn’t hang (infinite recursion) and still executes the real tool. > > <sup>Written by [Cursor Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit b241665. This will update automatically on new commits. Configure [here](https://cursor.com/dashboard?tab=bugbot).</sup> <!-- /CURSOR_SUMMARY --> --------- Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Summary
not_found_auto_installpreserves the shims directory in PATH,mise x -- toolcan enter an infinite loop on Unix, eventually hittingE2BIG("Argument list too long")exec_program(parity with the existing Windows fix from3ec95cf9a). This prevents recursion through shims.ts.which_bin()inExec::run()before exec. This prevents recursion through wrapper scripts (e.g.,.devcontainer/bin/tool) that callmise x -- tool— the wrapper precedes mise install paths in PATH, so without resolution the wrapper is found again instead of the real binary.test_exec_shim_recursion) mirroring the existing Windows testTest plan
cargo checkpassescargo test— all 481 tests passcoder ssh jdx.devcontainer -- 'claude --version'returns2.1.50 (Claude Code)immediately (previously hung in infinite recursion)./e2e/run_test cli/test_exec_shim_recursionpassestest_exec_chdir,test_exec_latest,test_shims_fallback)🤖 Generated with Claude Code
Note
Medium Risk
Touches core exec/path resolution behavior; mistakes could change which binary gets executed or break command lookup on Unix in edge PATH configurations.
Overview
Fixes a Unix-only
mise xinfinite-recursion failure mode by ensuring the executed program resolves to the real tool binary rather than re-hitting shims or wrapper scripts earlier inPATH.Exec::run()now pre-resolves bare program names to the mise-managed binary viats.which_bin()(Unix only), and Unixexec_programnow resolves the executable using aPATHwith the shims directory removed for lookup only while still passing the full environment to the child process. Adds an e2e regression test (e2e/cli/test_exec_shim_recursion) that asserts a real binary is chosen over a fake shim when shims appear first inPATH.Written by Cursor Bugbot for commit d53b221. This will update automatically on new commits. Configure here.