Skip to content

fix(exec): strip shims from PATH on Unix to prevent infinite recursion#8276

Merged
jdx merged 3 commits intomainfrom
fix/unix-exec-shim-recursion
Feb 21, 2026
Merged

fix(exec): strip shims from PATH on Unix to prevent infinite recursion#8276
jdx merged 3 commits intomainfrom
fix/unix-exec-shim-recursion

Conversation

@jdx
Copy link
Owner

@jdx jdx commented Feb 21, 2026

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

  • cargo check passes
  • cargo test — all 481 tests pass
  • All pre-commit lints pass (shfmt, shellcheck, cargo-fmt, etc.)
  • 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


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 x infinite-recursion failure mode by ensuring the executed program resolves to the real tool binary rather than re-hitting shims or wrapper scripts earlier in PATH.

Exec::run() now pre-resolves bare program names to the mise-managed binary via ts.which_bin() (Unix only), and Unix exec_program now resolves the executable using a PATH with 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 in PATH.

Written by Cursor Bugbot for commit d53b221. This will update automatically on new commits. Configure here.

Copilot AI review requested due to automatic review settings February 21, 2026 14:54
@gemini-code-assist
Copy link
Contributor

Summary of Changes

Hello @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 mise x -- tool commands on Unix systems. By intelligently filtering the shims directory from the PATH during internal program lookup, it ensures that the correct executable is identified, preventing system resource exhaustion and improving the robustness of tool execution.

Highlights

  • Infinite Recursion Fix: Addressed an infinite recursion bug in mise x -- tool on Unix where shims in PATH could cause the wrapper script to be repeatedly executed instead of the real binary, leading to an E2BIG error.
  • PATH Handling for Program Resolution: Implemented a mechanism to strip the shims directory from PATH during program resolution within mise x, ensuring the real binary is found. The full PATH (including shims) is still passed to the child process.
  • Regression Test Added: Added a new Unix end-to-end regression test (test_exec_shim_recursion) to prevent future regressions of this issue, mirroring an existing Windows test.
Changelog
  • e2e/cli/test_exec_shim_recursion
    • Added a new end-to-end test to verify that mise x correctly resolves tools and avoids infinite recursion when shims are present in the PATH.
  • src/cli/exec.rs
    • Modified the exec command logic to filter out the shims directory from the PATH when resolving the program to execute, preventing recursive calls to shims.
    • Ensured that the full PATH (including shims) is still passed to the child process for its own subprocesses.
Activity
  • Author confirmed cargo check passes.
  • Author confirmed cargo test passes all 481 tests.
  • Author confirmed all pre-commit lints pass.
  • Author confirmed ./e2e/run_test cli/test_exec_shim_recursion passes.
  • Author confirmed existing exec e2e tests still pass (test_exec_chdir, test_exec_latest, test_shims_fallback).
  • Author confirmed testing on Coder workspace: coder ssh jdx.devcontainer -- 'claude --version'.
Using Gemini Code Assist

The 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 /gemini <command> or @gemini-code-assist <command>. Below is a summary of the supported commands on the current page.

Feature Command Description
Code Review /gemini review Performs a code review for the current pull request in its current state.
Pull Request Summary /gemini summary Provides a summary of the current pull request in its current state.
Comment @gemini-code-assist Responds in comments when explicitly tagged, both in pull request comments and review comments.
Help /gemini help Displays a list of available commands.

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 .gemini/ folder in the base of the repository. Detailed instructions can be found here.

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

  1. Review the Privacy Notices, Generative AI Prohibited Use Policy, Terms of Service, and learn how to configure Gemini Code Assist in GitHub here. Gemini can make mistakes, so double check it and use code with caution.

Copy link
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

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.

Comment on lines +198 to +214
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
}
};
Copy link
Contributor

Choose a reason for hiding this comment

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

medium

The program resolution logic for Unix can be improved for robustness and consistency with the Windows implementation.

  1. Path Expansion: Using crate::file::replace_path(p) when filtering PATH ensures that entries containing ~ are correctly handled, which is important for robustness and matches the Windows implementation.
  2. Error Handling: Removing the fallback to the original program name when resolution fails is safer. If which_in cannot find the executable outside of the shims directory, falling back to the original name will likely cause execvp to 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()
    };

Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

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_program function (matching Windows fix from commit 3ec95cf)
  • Add Unix e2e regression test test_exec_shim_recursion to 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.

Comment on lines +204 to +206
let shims_dir = &*crate::dirs::SHIMS;
let filtered: Vec<_> = std::env::split_paths(&OsString::from(path_val))
.filter(|p| p != shims_dir)
Copy link

Copilot AI Feb 21, 2026

Choose a reason for hiding this comment

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

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.

Suggested change
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)

Copilot uses AI. Check for mistakes.
Comment on lines +210 to +213
match which::which_in(&program, lookup_path, cwd) {
Ok(resolved) => resolved.into_os_string(),
Err(_) => program, // Fall back to original if resolution fails
}
Copy link

Copilot AI Feb 21, 2026

Choose a reason for hiding this comment

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

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.

Suggested change
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()

Copilot uses AI. Check for mistakes.
let filtered: Vec<_> = std::env::split_paths(&OsString::from(path_val))
.filter(|p| p != shims_dir)
.collect();
std::env::join_paths(&filtered).unwrap()
Copy link

Copilot AI Feb 21, 2026

Choose a reason for hiding this comment

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

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.

Suggested change
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)
}
}

Copilot uses AI. Check for mistakes.
Copy link

@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.

Bugbot Autofix is ON, but a Cloud Agent failed to start.

@github-actions
Copy link

github-actions bot commented Feb 21, 2026

Hyperfine Performance

mise x -- echo

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%

@jdx jdx enabled auto-merge (squash) February 21, 2026 15:22
jdx and others added 2 commits February 21, 2026 10:07
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>
@jdx jdx force-pushed the fix/unix-exec-shim-recursion branch from 683b3f9 to f75c105 Compare February 21, 2026 16:08
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>
@jdx jdx force-pushed the fix/unix-exec-shim-recursion branch from f75c105 to d53b221 Compare February 21, 2026 16:29
@jdx jdx merged commit ab63c6b into main Feb 21, 2026
35 checks passed
@jdx jdx deleted the fix/unix-exec-shim-recursion branch February 21, 2026 16:42
jdx pushed a commit that referenced this pull request Feb 21, 2026
### 🚀 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)
netbsd-srcmastr pushed a commit to NetBSD/pkgsrc that referenced this pull request Feb 22, 2026
## [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)
jdx added a commit that referenced this pull request Feb 25, 2026
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>
jdx added a commit that referenced this pull request Feb 25, 2026
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>
jdx added a commit that referenced this pull request Feb 25, 2026
)

## 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>
risu729 pushed a commit to risu729/mise that referenced this pull request Feb 27, 2026
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>
risu729 pushed a commit to risu729/mise that referenced this pull request Feb 27, 2026
### 🚀 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)
risu729 pushed a commit to risu729/mise that referenced this pull request Feb 27, 2026
…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>
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.

2 participants