Skip to content

fix(exec): strip shims from PATH to prevent recursive shim execution#8189

Merged
jdx merged 6 commits into
mainfrom
fix/shim-exec-recursion
Feb 17, 2026
Merged

fix(exec): strip shims from PATH to prevent recursive shim execution#8189
jdx merged 6 commits into
mainfrom
fix/shim-exec-recursion

Conversation

@jdx

@jdx jdx commented Feb 16, 2026

Copy link
Copy Markdown
Owner

Summary

  • Strip the shims directory from PATH in Exec::run() before calling exec_program, preventing infinite process spawning on Windows
  • On Windows with "file" mode shims, shim scripts call mise x -- tool "$@" which re-enters Exec::run(). When not_found_auto_install preserves the shims directory in PATH, exec_program resolves "tool" back to the shim instead of the real binary, creating an infinite loop of child processes
  • The handle_shim() code path already had this protection (skipping shims dir at src/shims.rs:82-87), but the Exec code path used by Windows "file" mode shims was missing it

Test plan

  • Added e2e-win/shim_recursion.Tests.ps1 that creates a fake tool and shim with shims-before-tool PATH ordering, verifying mise x resolves the real tool instead of recursing
  • Verify on Windows with Git Bash: tool managed by another toolchain + auto_install=true no longer causes infinite process spawning

🤖 Generated with Claude Code


Note

Medium Risk
Touches Windows command resolution in exec_program; incorrect path filtering could cause commands not to be found or resolve differently on Windows, though the change is scoped and covered by a new regression test.

Overview
Prevents Windows "file"-mode shims from recursively re-invoking themselves by filtering the shims directory out of PATH only for which::which_in resolution inside exec_program (while leaving the full PATH intact for the spawned child process).

Adds a Windows e2e regression test (e2e-win/shim_recursion.Tests.ps1) that places shims ahead of a real tool on PATH and asserts mise x -- mytool executes the real binary output rather than the shim.

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

On Windows, "file" mode shims are bash scripts that call `mise x -- tool`,
which re-enters Exec::run(). When not_found_auto_install preserves the shims
directory in PATH, exec_program resolves "tool" back to the shim instead of
the real binary, causing infinite process spawning.

Strip the shims directory from PATH before exec_program, mirroring the
exclusion that which_shim() already performs in handle_shim().

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings February 16, 2026 21:41
@gemini-code-assist

Copy link
Copy Markdown
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 issue on Windows where the mise x command could enter an infinite recursion loop when attempting to execute tools managed by 'file' mode shims. The core problem stemmed from the PATH environment variable not being correctly sanitized, causing the system to repeatedly resolve a tool to its shim instead of the underlying binary. The solution involves proactively filtering the shims directory from the PATH within the execution logic, ensuring that the correct tool is always found and executed.

Highlights

  • Prevented infinite recursion on Windows: The Exec::run() function now strips the shims directory from the PATH environment variable before executing a program. This prevents an infinite loop where mise x -- tool on Windows (using 'file' mode shims) would re-enter Exec::run() and resolve the tool back to its shim instead of the actual binary.
  • Addressed specific Windows behavior: This fix specifically targets Windows environments where 'file' mode shims are bash scripts that call mise x -- tool "$@", and not_found_auto_install preserves the shims directory in PATH, leading to recursive shim execution.
  • New regression test added: A new end-to-end PowerShell test (e2e-win/shim_recursion.Tests.ps1) was added to verify that mise x correctly resolves the real tool and does not recurse through shims, even when shims are ordered before the tool in the PATH.
Changelog
  • e2e-win/shim_recursion.Tests.ps1
    • Added a new PowerShell test to simulate the problematic scenario of shim recursion on Windows.
    • The test creates a fake tool and a corresponding shim, then verifies that mise x correctly executes the real tool without infinite looping.
  • src/cli/exec.rs
    • Imported the dirs module to access the shims directory path.
    • Implemented logic within Exec::run() to filter the dirs::SHIMS directory from the PATH environment variable before executing a program.
    • Added comments explaining the rationale behind stripping shims from PATH to prevent recursive execution on Windows.
Activity
  • No activity has occurred on this pull request yet.
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.

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

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.

Code Review

This pull request addresses a critical issue of infinite recursion when executing shims on Windows. The approach of stripping the shims directory from the PATH within Exec::run() is a solid solution to prevent exec_program from resolving back to the shim itself. The addition of the end-to-end test e2e-win/shim_recursion.Tests.ps1 is excellent, as it provides a concrete regression test for this specific scenario.

I have one suggestion to improve the robustness of the implementation in src/cli/exec.rs regarding path canonicalization. Otherwise, the changes look good.

Comment thread src/cli/exec.rs Outdated
Comment on lines +135 to +140
let shims_dir = dirs::SHIMS
.canonicalize()
.unwrap_or_else(|_| dirs::SHIMS.to_path_buf());
let filtered: Vec<_> = std::env::split_paths(&OsString::from(path_val))
.filter(|p| p.canonicalize().unwrap_or_default() != shims_dir)
.collect();

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.

high

This implementation has a potential correctness issue if dirs::SHIMS.canonicalize() fails. In that case, shims_dir will hold a non-canonicalized path, while paths from PATH are still canonicalized inside the filter. Comparing canonicalized and non-canonicalized paths can lead to incorrect results (e.g., not stripping the shims path when it's a symlink).

Additionally, using unwrap_or_default() on a failed canonicalize call for a path in PATH is a bit risky. Using map_or would be safer and more explicit about handling paths that can't be canonicalized.

I suggest refactoring this logic to handle the canonicalization failure of the shims directory explicitly by falling back to a non-canonical comparison. This improves the robustness of the fix.

            let shims_dir = dirs::SHIMS.to_path_buf();
            let filtered: Vec<_> = if let Ok(shims_dir_canonical) = shims_dir.canonicalize() {
                std::env::split_paths(&OsString::from(path_val))
                    .filter(|p| p.canonicalize().map_or(true, |cp| cp != shims_dir_canonical))
                    .collect()
            } else {
                // fallback to non-canonical comparison if shims dir can't be canonicalized
                std::env::split_paths(&OsString::from(path_val))
                    .filter(|p| *p != shims_dir)
                    .collect()
            };

Copilot AI left a comment

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.

Pull request overview

This PR fixes an infinite recursion bug on Windows when using "file" mode shims with not_found_auto_install enabled. When a shim script calls mise x -- tool, and the shims directory remains in PATH, the exec command would resolve "tool" back to the shim instead of the real binary, creating an infinite loop.

Changes:

  • Strips the shims directory from PATH in Exec::run() before executing programs to prevent recursive shim execution
  • Adds a Windows end-to-end test that verifies mise x correctly resolves tools when shims appear before real tools in PATH

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated 8 comments.

File Description
src/cli/exec.rs Added PATH filtering logic to remove shims directory before program execution, preventing infinite recursion with Windows file-mode shims
e2e-win/shim_recursion.Tests.ps1 Added regression test that creates a fake shim and tool to verify mise x doesn't recurse through shims

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread src/cli/exec.rs Outdated
if let Some(path_val) = env.get(&*env::PATH_KEY) {
let shims_dir = dirs::SHIMS
.canonicalize()
.unwrap_or_else(|_| dirs::SHIMS.to_path_buf());

Copilot AI Feb 16, 2026

Copy link

Choose a reason for hiding this comment

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

The canonicalization error handling differs from the existing pattern in src/shims.rs:83-84 and may not correctly filter shims in all cases. The current code uses unwrap_or_default() for PATH entries but unwrap_or_else(|_| dirs::SHIMS.to_path_buf()) for the shims directory. This means if a PATH entry pointing to the shims directory can't be canonicalized (e.g., shims don't exist yet), it becomes an empty PathBuf while shims_dir becomes the non-canonical path, so they won't match and the shims path won't be filtered. Consider either: (1) using unwrap_or_default() for both like shims.rs does, or (2) falling back to string path comparison when canonicalization fails on either side.

Suggested change
.unwrap_or_else(|_| dirs::SHIMS.to_path_buf());
.unwrap_or_default();

Copilot uses AI. Check for mistakes.
Comment thread e2e-win/shim_recursion.Tests.ps1 Outdated
# which would cause infinite process spawning on Windows.

BeforeAll {
$originalPath = Get-Location

Copilot AI Feb 16, 2026

Copy link

Choose a reason for hiding this comment

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

The variable $originalPath should use the $script: scope modifier for consistency with other test files in the codebase (see e2e-win/prepare.Tests.ps1:4 which uses $script:originalPath). Without this, the variable might not be accessible in the AfterAll block depending on Pester's scoping rules, potentially causing cleanup to fail.

Copilot uses AI. Check for mistakes.
Comment thread e2e-win/shim_recursion.Tests.ps1 Outdated
Set-Location TestDrive:
$env:MISE_TRUSTED_CONFIG_PATHS = $TestDrive

$shimPath = Join-Path -Path $env:MISE_DATA_DIR -ChildPath "shims"

Copilot AI Feb 16, 2026

Copy link

Choose a reason for hiding this comment

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

The variable $shimPath is declared in BeforeAll but also used in AfterAll. For consistency with other test files in the codebase (see e2e-win/prepare.Tests.ps1:4), consider using the $script: scope modifier to make the scoping explicit.

Copilot uses AI. Check for mistakes.
Comment thread e2e-win/shim_recursion.Tests.ps1 Outdated

AfterAll {
# Clean up the fake shim
Remove-Item -Path (Join-Path $shimPath "mytool.cmd") -ErrorAction SilentlyContinue

Copilot AI Feb 16, 2026

Copy link

Choose a reason for hiding this comment

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

The AfterAll cleanup is incomplete. The test creates a $toolDir directory with a tool binary in BeforeAll (line 14), but only cleans up the shim in AfterAll. The $toolDir directory and its contents should also be removed to ensure proper test cleanup, similar to how other test files clean up their created directories (see e2e-win/config_ceiling_paths.Tests.ps1:40).

Suggested change
Remove-Item -Path (Join-Path $shimPath "mytool.cmd") -ErrorAction SilentlyContinue
Remove-Item -Path (Join-Path $shimPath "mytool.cmd") -ErrorAction SilentlyContinue
# Clean up the fake tool directory and its contents
Remove-Item -Path $toolDir -Recurse -ErrorAction SilentlyContinue

Copilot uses AI. Check for mistakes.
Comment thread e2e-win/shim_recursion.Tests.ps1 Outdated
$shimPath = Join-Path -Path $env:MISE_DATA_DIR -ChildPath "shims"

# Create a fake "mytool" binary that just echoes a marker
$toolDir = Join-Path $TestDrive "toolbin"

Copilot AI Feb 16, 2026

Copy link

Choose a reason for hiding this comment

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

The variable $toolDir is declared in BeforeAll but may not be accessible in AfterAll for cleanup without the $script: scope modifier. Consider declaring it as $script:toolDir for consistency with other test files and to ensure it can be properly cleaned up in AfterAll.

Copilot uses AI. Check for mistakes.
Comment thread e2e-win/shim_recursion.Tests.ps1 Outdated
AfterAll {
# Clean up the fake shim
Remove-Item -Path (Join-Path $shimPath "mytool.cmd") -ErrorAction SilentlyContinue
Set-Location $originalPath

Copilot AI Feb 16, 2026

Copy link

Choose a reason for hiding this comment

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

The reference to $originalPath should be $script:originalPath to match the declaration scope that should be used in BeforeAll. This ensures proper scoping and consistency with other test files in the codebase.

Copilot uses AI. Check for mistakes.
Comment thread src/cli/exec.rs Outdated
Comment on lines +141 to +148
if let Ok(new_path) = std::env::join_paths(&filtered) {
env.insert(
env::PATH_KEY.to_string(),
new_path.to_string_lossy().to_string(),
);
}
}

Copilot AI Feb 16, 2026

Copy link

Choose a reason for hiding this comment

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

If std::env::join_paths fails, the PATH will remain unchanged with the shims directory still present, potentially causing the exact recursion issue this code is meant to prevent. While join_paths failures are rare (only occurring when paths contain the platform's path separator), consider either using .unwrap() to match the pattern in src/plugins/script_manager.rs:124 and src/fake_asdf_windows.rs:23, or logging a warning if the join fails so the issue is visible in diagnostics.

Suggested change
if let Ok(new_path) = std::env::join_paths(&filtered) {
env.insert(
env::PATH_KEY.to_string(),
new_path.to_string_lossy().to_string(),
);
}
}
let new_path = std::env::join_paths(&filtered).unwrap();
env.insert(
env::PATH_KEY.to_string(),
new_path.to_string_lossy().to_string(),
);
}

Copilot uses AI. Check for mistakes.
Comment thread src/cli/exec.rs Outdated
Comment on lines +131 to +133
// On Windows, "file" mode shims are bash scripts that call `mise x -- tool "$@"`,
// which re-enters Exec. If shims remain in PATH (due to not_found_auto_install),
// exec_program would resolve "tool" back to the shim, causing an infinite loop.

Copilot AI Feb 16, 2026

Copy link

Choose a reason for hiding this comment

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

The comment mentions "bash scripts" but Windows "file" mode shims actually create both bash scripts (without extension, for Git Bash/Cygwin) and .cmd files (for Windows Command Prompt/PowerShell). Both types call mise x -- tool and can cause the recursion issue. Consider updating the comment to clarify this, or simply say "shim scripts" to be more general.

Suggested change
// On Windows, "file" mode shims are bash scripts that call `mise x -- tool "$@"`,
// which re-enters Exec. If shims remain in PATH (due to not_found_auto_install),
// exec_program would resolve "tool" back to the shim, causing an infinite loop.
// On Windows, "file" mode shims create shim scripts (for bash/Git Bash/Cygwin and .cmd)
// that call `mise x -- tool "$@"`, which re-enters Exec. If shims remain in PATH
// (due to not_found_auto_install), exec_program would resolve "tool" back to the shim,
// causing an infinite loop.

Copilot uses AI. Check for mistakes.
jdx and others added 2 commits February 16, 2026 21:52
- Improve canonicalization robustness: fall back to non-canonical
  comparison when shims dir can't be canonicalized, use map_or to
  safely handle PATH entries that fail canonicalization
- Use unwrap() for join_paths since failure is effectively impossible
- Fix PowerShell test scoping: use $script: for variables shared
  across BeforeAll/AfterAll blocks
- Add cleanup of toolDir in AfterAll

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The previous approach stripped shims from the child's PATH env, which
broke the shims fallback feature (test_shims_fallback) where subprocesses
need shims in PATH to find other tools.

Instead, strip shims only from the which::which_in lookup path used to
resolve the program binary. The child process still inherits the full
PATH with shims so subprocesses can trigger auto-install.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@jdx jdx enabled auto-merge (squash) February 16, 2026 22:03
@github-actions

github-actions Bot commented Feb 16, 2026

Copy link
Copy Markdown

Hyperfine Performance

mise x -- echo

Command Mean [ms] Min [ms] Max [ms] Relative
mise-2026.2.13 x -- echo 24.0 ± 0.4 23.2 26.5 1.00
mise x -- echo 24.6 ± 0.8 23.4 31.1 1.02 ± 0.04

mise env

Command Mean [ms] Min [ms] Max [ms] Relative
mise-2026.2.13 env 23.6 ± 0.6 22.7 28.5 1.00
mise env 23.8 ± 0.5 22.8 25.8 1.01 ± 0.03

mise hook-env

Command Mean [ms] Min [ms] Max [ms] Relative
mise-2026.2.13 hook-env 24.1 ± 0.4 23.3 26.6 1.00
mise hook-env 24.5 ± 0.6 23.4 26.7 1.02 ± 0.03

mise ls

Command Mean [ms] Min [ms] Max [ms] Relative
mise-2026.2.13 ls 22.3 ± 0.5 21.6 26.5 1.00
mise ls 22.9 ± 0.9 22.0 38.7 1.03 ± 0.05

xtasks/test/perf

Command mise-2026.2.13 mise Variance
install (cached) 129ms 132ms -2%
ls (cached) 81ms 82ms -1%
bin-paths (cached) 84ms 85ms -1%
task-ls (cached) 824ms 812ms +1%

jdx and others added 3 commits February 16, 2026 22:30
Canonicalization can fail on Windows for various reasons, and Rust's
PathBuf equality is case-sensitive even on Windows. Use simple
case-insensitive string comparison which reliably handles Windows
path casing differences.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Use a non-recursive shim (echoes a marker instead of calling mise x)
so the test fails fast with a clear assertion error if the fix doesn't
work, rather than hanging the CI with infinite process spawning.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
On CI, MISE_DATA_DIR is set to ~/.local/share/mise so PATH entries
contain unexpanded ~ while dirs::SHIMS has ~ already expanded via
var_path/replace_path. Use file::replace_path to expand ~ in PATH
entries and normalize separators (/ vs \) for reliable comparison.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@jdx jdx merged commit 3ec95cf into main Feb 17, 2026
35 checks passed
@jdx jdx deleted the fix/shim-exec-recursion branch February 17, 2026 02:22
mise-en-dev added a commit that referenced this pull request Feb 17, 2026
### 🚀 Features

- **(task)** stream keep-order output in real-time per task by @jdx in
[#8164](#8164)

### 🐛 Bug Fixes

- **(aqua)** resolve lockfile artifacts for target platform (fix
discussion #7479) by @mackwic in
[#8183](#8183)
- **(exec)** strip shims from PATH to prevent recursive shim execution
by @jdx in [#8189](#8189)
- **(hook-env)** preserve PATH reordering done after activation by @jdx
in [#8190](#8190)
- **(lockfile)** resolve version aliases before lockfile lookup by @jdx
in [#8194](#8194)
- **(registry)** set helm-diff archive bin name to diff by @jean-humann
in [#8173](#8173)
- **(task)** improve source freshness checks with dynamic task dirs by
@rooperuu in [#8169](#8169)
- **(task)** resolve global tasks when running from monorepo root by
@jdx in [#8192](#8192)
- **(task)** prevent wildcard glob `test:*` from matching parent task
`test` by @jdx in [#8165](#8165)
- **(task)** resolve task_config.includes relative to config root by
@jdx in [#8193](#8193)
- **(upgrade)** skip untrusted tracked configs during upgrade by @jdx in
[#8195](#8195)

### 🚜 Refactor

- use enum for npm.pacakge_manager by @risu729 in
[#8180](#8180)

### 📚 Documentation

- **(plugins)** replace node/asdf-nodejs examples with vfox plugins by
@jdx in [#8191](#8191)

### ⚡ Performance

- call npm view only once by @risu729 in
[#8181](#8181)

### New Contributors

- @jean-humann made their first contribution in
[#8173](#8173)
- @mackwic made their first contribution in
[#8183](#8183)
- @rooperuu made their first contribution in
[#8169](#8169)

## 📦 Aqua Registry Updates

#### New Packages (2)

- [`BetterDiscord/cli`](https://github.com/BetterDiscord/cli)
- [`glossia.ai/cli`](https://github.com/glossia.ai/cli)
lucasew pushed a commit to lucasew/CONTRIB-mise that referenced this pull request Feb 18, 2026
…dx#8189)

## Summary

- Strip the shims directory from PATH in `Exec::run()` before calling
`exec_program`, preventing infinite process spawning on Windows
- On Windows with "file" mode shims, shim scripts call `mise x -- tool
"$@"` which re-enters `Exec::run()`. When `not_found_auto_install`
preserves the shims directory in PATH, `exec_program` resolves "tool"
back to the shim instead of the real binary, creating an infinite loop
of child processes
- The `handle_shim()` code path already had this protection (skipping
shims dir at `src/shims.rs:82-87`), but the `Exec` code path used by
Windows "file" mode shims was missing it

## Test plan
- [ ] Added `e2e-win/shim_recursion.Tests.ps1` that creates a fake tool
and shim with shims-before-tool PATH ordering, verifying `mise x`
resolves the real tool instead of recursing
- [ ] Verify on Windows with Git Bash: tool managed by another toolchain
+ `auto_install=true` no longer causes infinite process spawning

🤖 Generated with [Claude Code](https://claude.com/claude-code)

<!-- CURSOR_SUMMARY -->
---

> [!NOTE]
> **Medium Risk**
> Changes Windows `exec_program` command resolution by filtering `PATH`
during lookup; mistakes could cause commands to resolve differently or
fail to launch on Windows. Risk is mitigated by limiting the filter to
lookup-only and adding an e2e regression test.
> 
> **Overview**
> Prevents Windows `mise x` from recursively re-invoking shim scripts by
filtering the shims directory out of `PATH` **only for executable
resolution** before calling `which::which_in`, while still passing the
original `PATH` through to the spawned process.
> 
> Adds a Windows e2e regression test
(`e2e-win/shim_recursion.Tests.ps1`) that creates a
shim-before-real-tool `PATH` scenario and asserts `mise x -- mytool`
executes the real tool output instead of hanging via infinite recursion.
> 
> <sup>Written by [Cursor
Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit
e97b67d. 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>
lucasew pushed a commit to lucasew/CONTRIB-mise that referenced this pull request Feb 18, 2026
### 🚀 Features

- **(task)** stream keep-order output in real-time per task by @jdx in
[jdx#8164](jdx#8164)

### 🐛 Bug Fixes

- **(aqua)** resolve lockfile artifacts for target platform (fix
discussion jdx#7479) by @mackwic in
[jdx#8183](jdx#8183)
- **(exec)** strip shims from PATH to prevent recursive shim execution
by @jdx in [jdx#8189](jdx#8189)
- **(hook-env)** preserve PATH reordering done after activation by @jdx
in [jdx#8190](jdx#8190)
- **(lockfile)** resolve version aliases before lockfile lookup by @jdx
in [jdx#8194](jdx#8194)
- **(registry)** set helm-diff archive bin name to diff by @jean-humann
in [jdx#8173](jdx#8173)
- **(task)** improve source freshness checks with dynamic task dirs by
@rooperuu in [jdx#8169](jdx#8169)
- **(task)** resolve global tasks when running from monorepo root by
@jdx in [jdx#8192](jdx#8192)
- **(task)** prevent wildcard glob `test:*` from matching parent task
`test` by @jdx in [jdx#8165](jdx#8165)
- **(task)** resolve task_config.includes relative to config root by
@jdx in [jdx#8193](jdx#8193)
- **(upgrade)** skip untrusted tracked configs during upgrade by @jdx in
[jdx#8195](jdx#8195)

### 🚜 Refactor

- use enum for npm.pacakge_manager by @risu729 in
[jdx#8180](jdx#8180)

### 📚 Documentation

- **(plugins)** replace node/asdf-nodejs examples with vfox plugins by
@jdx in [jdx#8191](jdx#8191)

### ⚡ Performance

- call npm view only once by @risu729 in
[jdx#8181](jdx#8181)

### New Contributors

- @jean-humann made their first contribution in
[jdx#8173](jdx#8173)
- @mackwic made their first contribution in
[jdx#8183](jdx#8183)
- @rooperuu made their first contribution in
[jdx#8169](jdx#8169)

## 📦 Aqua Registry Updates

#### New Packages (2)

- [`BetterDiscord/cli`](https://github.com/BetterDiscord/cli)
- [`glossia.ai/cli`](https://github.com/glossia.ai/cli)
risu729 pushed a commit to risu729/mise that referenced this pull request Feb 27, 2026
### 🚀 Features

- **(task)** stream keep-order output in real-time per task by @jdx in
[jdx#8164](jdx#8164)

### 🐛 Bug Fixes

- **(aqua)** resolve lockfile artifacts for target platform (fix
discussion jdx#7479) by @mackwic in
[jdx#8183](jdx#8183)
- **(exec)** strip shims from PATH to prevent recursive shim execution
by @jdx in [jdx#8189](jdx#8189)
- **(hook-env)** preserve PATH reordering done after activation by @jdx
in [jdx#8190](jdx#8190)
- **(lockfile)** resolve version aliases before lockfile lookup by @jdx
in [jdx#8194](jdx#8194)
- **(registry)** set helm-diff archive bin name to diff by @jean-humann
in [jdx#8173](jdx#8173)
- **(task)** improve source freshness checks with dynamic task dirs by
@rooperuu in [jdx#8169](jdx#8169)
- **(task)** resolve global tasks when running from monorepo root by
@jdx in [jdx#8192](jdx#8192)
- **(task)** prevent wildcard glob `test:*` from matching parent task
`test` by @jdx in [jdx#8165](jdx#8165)
- **(task)** resolve task_config.includes relative to config root by
@jdx in [jdx#8193](jdx#8193)
- **(upgrade)** skip untrusted tracked configs during upgrade by @jdx in
[jdx#8195](jdx#8195)

### 🚜 Refactor

- use enum for npm.pacakge_manager by @risu729 in
[jdx#8180](jdx#8180)

### 📚 Documentation

- **(plugins)** replace node/asdf-nodejs examples with vfox plugins by
@jdx in [jdx#8191](jdx#8191)

### ⚡ Performance

- call npm view only once by @risu729 in
[jdx#8181](jdx#8181)

### New Contributors

- @jean-humann made their first contribution in
[jdx#8173](jdx#8173)
- @mackwic made their first contribution in
[jdx#8183](jdx#8183)
- @rooperuu made their first contribution in
[jdx#8169](jdx#8169)

## 📦 Aqua Registry Updates

#### New Packages (2)

- [`BetterDiscord/cli`](https://github.com/BetterDiscord/cli)
- [`glossia.ai/cli`](https://github.com/glossia.ai/cli)
jdx pushed a commit that referenced this pull request Mar 30, 2026
…8802)

## Summary

Three code paths spawn subprocesses that can invoke mise-managed tools
(e.g. `gh`, `git`) without stripping mise shims from PATH. When the tool
resolves to a mise shim, it re-enters mise, which may trigger the same
subprocess again — causing **infinite recursion (fork bomb)**.

Observed: load average >1800 on an ARM SBC, system unresponsive. Also
reproduced on macOS.

## Root Cause

Three subprocess-spawning paths inherit shims in PATH:

### 1. `credential_command` in `src/github.rs`
`get_credential_command_token()` runs `sh -c <cmd>` (e.g. `gh auth
token`). If `gh` is a mise shim → recursion.

### 2. `git credential fill` in `src/github.rs`
`get_git_credential_token()` runs `git credential fill`. If `git` is a
mise shim, or git's credential helper invokes `gh` (via `gh auth
setup-git`) → recursion.

### 3. `exec()` template function in `src/tera.rs` (primary trigger)
When a `.mise.toml` contains:
```toml
[env]
GITHUB_TOKEN = "{{exec(command='gh auth token')}}"
```
Every `mise hook-env` in that directory runs `gh auth token` via
`tera_exec()` with `PRISTINE_ENV`, which includes shims in PATH → `gh`
shim → `mise exec` → evaluates env → runs `gh auth token` again →
infinite recursion.

This is the most common trigger because `exec()` in `[env]` is the
idiomatic way to derive env vars from CLI tools.

## Fix

Add a shared `file::path_env_without_shims()` helper (next to the
existing `which_no_shims()`) that filters `dirs::SHIMS` out of PATH and
returns an `OsString` suitable for `.env("PATH", ...)`. Used in all
three call sites:

- **`src/github.rs`**: `get_credential_command_token()` and
`get_git_credential_token()`
- **`src/tera.rs`**: `tera_exec()`

Follows the same shim-stripping pattern established in:
- PR #8475 (`dependency_env()`)
- PR #8276 / #8560 (`exec_program()`)
- PR #8189 (Windows variant)
- PR #8402 (`uv` venv creation via `which_no_shims()`)

Related: Discussion #6374 — same user-facing symptom ("Cannot fork")
from `exec()` path.

## Reproduction

### Tera exec path (most common)
1. Install `gh` via mise (`gh = "latest"`)
2. Create a project `.mise.toml` with `[env] GITHUB_TOKEN =
"{{exec(command='gh auth token')}}"`
3. `cd` into that directory — `mise hook-env` fires, evaluates the
template, runs `gh auth token` through the shim → fork bomb

### Credential command path
1. Install `gh` via mise, set `credential_command = "gh auth token"` in
mise settings
2. Trigger any GitHub API call → fork bomb

## Changes

- **`src/file.rs`**: New `path_env_without_shims()` public helper —
shared by all call sites
- **`src/github.rs`**: Use shared helper in
`get_credential_command_token()` and `get_git_credential_token()`
- **`src/tera.rs`**: Use shared helper in `tera_exec()`
- **`e2e/cli/test_github_credential_shim_recursion`**: e2e test for
credential_command path
- **`e2e/cli/test_tera_exec_shim_recursion`**: e2e test for tera exec()
path

## Note on env var workaround

Setting `MISE_GITHUB_TOKEN=""` does **not** prevent the hang —
`resolve_token()` filters empty strings with `.filter(|t|
!t.is_empty())`, so it falls through to subprocess-based fallbacks. A
non-empty sentinel value (e.g. `MISE_GITHUB_TOKEN="none"`) works for the
github.rs paths but the tera `exec()` path is independent of token
resolution entirely.

## Test plan

- [x] `cargo check` passes
- [ ] CI: `cargo test` passes  
- [ ] CI: e2e test `test_github_credential_shim_recursion` passes
- [ ] CI: e2e test `test_tera_exec_shim_recursion` passes
- [x] Manual test: project with `exec(command='gh auth token')` in
`.mise.toml`, `cd` does not fork-bomb

---------

Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.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