Skip to content

feat(install): add shared and system install directories#8581

Merged
jdx merged 15 commits intomainfrom
feat/shared-install-dirs
Mar 13, 2026
Merged

feat(install): add shared and system install directories#8581
jdx merged 15 commits intomainfrom
feat/shared-install-dirs

Conversation

@jdx
Copy link
Copy Markdown
Owner

@jdx jdx commented Mar 13, 2026

Summary

Adds support for shared, read-only tool install directories — useful for Docker containers, toolbox containers, and bastion hosts where a base set of tools is pre-installed in a shared location.

  • shared_install_dirs setting (MISE_SHARED_INSTALL_DIRS, colon-separated) for additional read-only directories to search for installed tool versions
  • System install directory at /usr/local/share/mise/installs (MISE_SYSTEM_DATA_DIR/installs) — automatically checked when it exists, no config needed
  • mise install --system — install tools to the system-wide shared directory
  • mise install --shared <path> — install tools to a custom shared directory
  • mise ls — shows (system) and (shared) labels for versions from shared directories
  • MISE_SYSTEM_CONFIG_DIR — renamed from MISE_SYSTEM_DIR for parity with MISE_SYSTEM_DATA_DIR (legacy env var still supported)
  • Feature is marked [experimental]

Closes #8549

How it works

  • Lookup order: primary install dir → system dir → user-configured shared dirs
  • Read-only: shared dirs are never written to during normal mise install; versions found there are treated as already installed
  • Install flags: --system and --shared install to alternate directories and write .mise-installs.toml manifests there
  • Config: works via env var or config file (shared_install_dirs = [...] in settings)
  • Display: mise ls annotates shared versions with (system) or (shared) in cyan

Test plan

  • All 531 unit tests pass
  • e2e test (test_shared_install_dirs) covers:
    • Shared versions appear in mise ls --installed
    • mise x executes tools from shared dirs
    • mise install skips versions already in shared dirs
    • Uninstalling primary versions doesn't affect shared versions
    • Shared versions remain usable after primary removal
  • All lint checks pass

🤖 Generated with Claude Code


Note

Medium Risk
Medium risk because it changes tool install/lookup paths and manifest handling, which can affect whether versions are considered installed and where --force writes. Also renames the system config env var with a legacy alias, which could impact deployments relying on the old name.

Overview
Adds experimental shared/system install directories so tool version resolution can fall back from the primary installs dir to /usr/local/share/mise/installs and any user-configured shared_install_dirs (MISE_SHARED_INSTALL_DIRS). Tool path resolution (ToolRequest::install_path, ToolVersion::install_path) and install-state discovery now incorporate these directories, with caching and manifest reading/writing updated to support per-directory .mise-installs.toml.

Extends mise install with --system and --shared <path> to install into alternate directories while ensuring --force doesn’t mutate shared installs unless explicitly targeted, and updates mise ls to label shared/system versions. Renames MISE_SYSTEM_DIR to MISE_SYSTEM_CONFIG_DIR (legacy alias retained), updates schema/settings/docs/manpages/completions, and adds an e2e test covering shared-dir fallback and force/uninstall behavior.

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

Add MISE_SHARED_INSTALL_DIRS setting to allow searching additional
read-only directories for installed tool versions. This enables shared
environments like Docker containers and bastion hosts to pre-install
tools in a shared location while users install additional tools in
their own directory.

Closes #8549

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

Summary of Changes

Hello, 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 enhances mise by introducing support for shared, read-only tool installation directories. This feature allows users to leverage pre-installed tool versions in environments like Docker containers or bastion hosts, where a base set of tools might be provided centrally. mise will now check these shared locations if a tool is not found in the primary installation directory, providing greater flexibility for tool management in diverse deployment scenarios without altering the core installation behavior for new tools.

Highlights

  • Shared Install Directories: Introduced MISE_SHARED_INSTALL_DIRS setting for specifying additional read-only directories where mise can find pre-installed tool versions.
  • Fallback Mechanism: Enabled mise to search these shared directories as a fallback if a tool is not found in the primary installation directory.
  • Installation Behavior: Ensured that new tool installations always target the primary install directory, maintaining read-only status for shared directories.
  • Manifest Support: Allowed shared directories to contain .mise-installs.toml manifests for backend metadata, enabling mise to understand tool configurations within these shared locations.
Changelog
  • e2e/cli/test_shared_install_dirs
    • Added a new end-to-end test to validate the functionality of shared install directories, including mise ls --installed and mise x commands.
  • schema/mise.json
    • Updated the JSON schema to include the new shared_install_dirs property, defining it as an array of strings.
  • settings.toml
    • Added a new shared_install_dirs setting with comprehensive documentation, specifying its environment variable (MISE_SHARED_INSTALL_DIRS), type, and parsing method.
  • src/backend/mod.rs
    • Modified the Backend::find_installed_version method to incorporate checking MISE_SHARED_INSTALL_DIRS for tool installations, after the primary install path.
  • src/env.rs
    • Defined a new MISE_SHARED_INSTALL_DIRS static Lazy variable to parse the colon-separated environment variable into a Vec<PathBuf>.
  • src/toolset/install_state.rs
    • Updated the init_tools function to scan the newly introduced shared install directories, merge any discovered tool versions with existing primary installs, and handle their respective .mise-installs.toml manifests.
  • src/toolset/tool_request.rs
    • Introduced a new helper function resolve_shared_install_path and integrated it into ToolRequest::install_path to ensure that if a tool is not found in the primary install path, shared directories are checked as a fallback.
  • src/toolset/tool_version.rs
    • Modified ToolVersion::install_path to also check MISE_SHARED_INSTALL_DIRS for existing tool versions if they are not found in the primary installation path.
Activity
  • All 531 unit tests passed.
  • A new end-to-end test (test_shared_install_dirs) was added and verified.
  • All lint checks passed.
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.

@greptile-apps
Copy link
Copy Markdown
Contributor

greptile-apps bot commented Mar 13, 2026

Greptile Summary

This PR adds experimental support for shared, read-only tool install directories in mise, targeting Docker/toolbox/bastion-host use-cases. It introduces shared_install_dirs (config + env), an auto-discovered system dir at /usr/local/share/mise/installs, mise install --system / --shared <path> flags, (system)/(shared) labels in mise ls, and a MISE_SYSTEM_CONFIG_DIR rename with legacy alias.

Key changes:

  • src/env.rs: two-tier helper functions (shared_install_dirs_early() for init time, shared_install_dirs() for runtime) plus find_in_shared_installs() and install_path_category()
  • src/toolset/install_state.rs: scans shared dirs during init_tools() and merges versions into the in-memory state; reset() now clears the new INSTALL_PATH_CACHE
  • src/toolset/tool_version.rs: INSTALL_PATH_CACHE to avoid redundant filesystem probes; install_path() falls back to shared dirs when the primary path is absent
  • src/backend/mod.rs: redirects --force away from shared dirs to primary, writes .mise-installs.toml manifests for shared/system installs

Issues found:

  • mise install --shared <path> does not write .mise-installs.toml to the target directory when <path> is not pre-registered in MISE_SHARED_INSTALL_DIRS or settings. install_path_category() returns Local for the fresh path, skipping the else if branch entirely. Tools with non-default backends (aqua, github, http) lose their full identifier in the manifest. --system is unaffected since the system dir is always recognized.
  • cleanup_install_dirs_on_error uses self.ba().installs_path (always the primary user dir) when deciding whether to prune an empty parent, even during --system/--shared installs. This can spuriously remove the user's empty primary tool dir on a failed shared install.

Confidence Score: 3/5

  • Mostly safe for the documented happy paths, but mise install --shared <fresh-path> silently omits the backend manifest for non-default backends, causing a correctness regression for that code path.
  • The core read-path (lookup order, mise ls labels, mise x execution from shared dirs, --force redirect) is solid and well-tested. Two logic bugs exist in the write path: (1) the manifest-write guard uses install_path_category which doesn't recognize a fresh --shared path that hasn't yet been added to settings, meaning .mise-installs.toml is never written for arbitrary --shared installs of non-default-backend tools; (2) cleanup_install_dirs_on_error references the primary user installs_path instead of the shared install target when pruning empty parent dirs. Neither bug causes data loss for standard tools or the --system flow, but the manifest omission breaks backend-metadata round-trips for --shared with tools like aqua/github/http.
  • src/backend/mod.rs (both the post-install manifest-write block at line 1082–1091 and the cleanup_install_dirs_on_error function at line 1271–1286)

Important Files Changed

Filename Overview
src/backend/mod.rs Two bugs found: (1) manifest not written after --shared <path> when the path isn't pre-registered in shared_install_dirs, silently losing backend metadata; (2) cleanup_install_dirs_on_error checks the primary user installs_path rather than the shared install target, potentially spuriously removing empty user dirs.
src/toolset/install_state.rs Shared dir scanning in init_tools() correctly uses shared_install_dirs_early() (documented early-boot variant). reset() now correctly calls reset_install_path_cache(), addressing cache-stale concerns. The two-phase design (early-boot vs. full settings) is clearly documented.
src/env.rs Introduces shared_install_dirs() (reads Settings + env) and shared_install_dirs_early() (env-only, used at init time), with clear documentation on the split. find_in_shared_installs and install_path_category are clean helpers. The --shared <fresh path> manifest-write bug stems from callers of install_path_category rather than this file itself.
src/toolset/toolset_install.rs Correctly applies opts.install_dir to set tv.install_path before delegating to the backend. The install_dir signal is set but not propagated to InstallContext, which is why the backend cannot distinguish "explicitly set via --shared" from "resolved to shared dir via find_in_shared_installs".
src/cli/install.rs New --shared and --system flags are correctly gated with conflicts_with, and install_opts() properly routes them into InstallOptions.install_dir. The experimental flags are clearly documented.
e2e/cli/test_shared_install_dirs Good coverage of the read-only happy path: shared versions appear in mise ls, execute correctly, prevent redundant primary install, and survive uninstall of primary. The --force redirect to primary dir is also tested. Does not test non-default backend manifest round-trips, which would exercise the manifest-write bug.

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart TD
    A["mise install tool@ver"] --> B{Install target flag?}
    B -->|"--system"| C["tv.install_path = SYSTEM_INSTALLS_DIR/tool/ver"]
    B -->|"--shared path"| D["tv.install_path = path/tool/ver"]
    B -->|"neither"| E["tv.install_path = None, resolved later"]

    C --> F["backend::install_version()"]
    D --> F
    E --> F

    F --> G{force and no explicit install_path and path is shared?}
    G -->|"yes"| H["Redirect to primary installs dir"]
    G -->|"no"| I["Proceed as-is"]
    H --> I

    I --> J["is_version_installed?"]
    J -->|"yes, no force"| K["Return early: already installed"]
    J -->|"no or force"| L["create_install_dirs + install"]

    L --> M["Post-install manifest write"]
    M --> N{starts with primary dirs::INSTALLS?}
    N -->|"yes"| O["write to primary manifest OK"]
    N -->|"no"| P{install_path_category != Local?}
    P -->|"System or known Shared"| Q["write to shared manifest OK"]
    P -->|"Local - fresh --shared path not yet in settings"| R["No manifest written - silent bug"]

    style R fill:#f88,stroke:#c00
Loading

Comments Outside Diff (2)

  1. src/backend/mod.rs, line 1082-1091 (link)

    Manifest not written for arbitrary --shared <path> installs

    When mise install --shared /some/fresh/path tool@version is used and /some/fresh/path is not already present in MISE_SHARED_INSTALL_DIRS or in Settings::shared_install_dirs, env::install_path_category(&install_path) returns InstallPathCategory::Local (because shared_install_dirs() doesn't know about the path yet). The else if branch is therefore skipped, and no .mise-installs.toml manifest is written to the target directory.

    This matters for tools with non-default backends (aqua, github, http, etc.) whose full backend identifier is stored exclusively in the manifest. Without the manifest, a second user pointing MISE_SHARED_INSTALL_DIRS at that path will fall back to the directory-name-as-short-name heuristic (line 347 in install_state.rs), silently losing the backend mapping.

    Note that --system is unaffected because its target path always starts with MISE_SYSTEM_INSTALLS_DIR.

    The root cause is that the guard uses a runtime lookup of configured dirs to decide whether to write, rather than using the authoritative signal that is already available: tv.install_path being Some(_) (set by toolset_install.rs whenever opts.install_dir is present).

    A minimal fix:

    let install_path = tv.install_path();
    if install_path.starts_with(*dirs::INSTALLS) {
        install_state::write_backend_meta(self.ba())?;
    } else if tv.install_path.is_some()                                         // ← explicitly set via --system/--shared
        || env::install_path_category(&install_path) != env::InstallPathCategory::Local
    {
        if let Some(installs_dir) = install_path.parent().and_then(|p| p.parent()) {
            let manifest = installs_dir.join(".mise-installs.toml");
            install_state::write_backend_meta_to(self.ba(), &manifest)?;
        }
    }
  2. src/backend/mod.rs, line 1271-1286 (link)

    cleanup_install_dirs_on_error removes wrong parent dir for --system/--shared installs

    self.ba().installs_path is always the primary user installs directory (e.g. ~/.local/share/mise/installs/tiny), even when the current installation target is a system or shared directory. If a --system install of a brand-new tool fails, this function will:

    1. Correctly delete the (partially created) system tool directory (tv.install_path()).
    2. Then check self.ba().installs_path — the user's tool install dir — and remove it if empty.

    This means a failed mise install --system tiny@2.0.0 could delete ~/.local/share/mise/installs/tiny if the user has never installed tiny before (it's empty). While the immediate impact is low (the directory can be recreated on the next install), it introduces an inconsistency.

    The check should guard against the shared case:

    fn cleanup_install_dirs_on_error(&self, tv: &ToolVersion) {
        if !Settings::get().always_keep_install {
            let _ = remove_all_with_warning(tv.install_path());
            let _ = file::remove_file(self.incomplete_file_path(tv));
            // Only clean up the primary installs path when we actually installed there
            let installs_path = &self.ba().installs_path;
            if installs_path.exists()
                && tv.install_path().starts_with(installs_path)   // ← new guard
                && let Ok(entries) = file::dir_subdirs(installs_path)
                && entries.is_empty()
            {
                let _ = remove_all_with_warning(installs_path);
            }
            self.cleanup_install_dirs(tv);
        }
    }

Fix All in Claude Code

Last reviewed commit: b70caa7

Copy link
Copy Markdown
Contributor

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

Choose a reason for hiding this comment

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

Code Review

This pull request introduces a useful feature for shared, read-only install directories, which is well-implemented across the configuration, schema, and core logic. The changes are logical and include a new end-to-end test to validate the functionality. My review focuses on a few opportunities to improve code structure and maintainability by addressing code duplication and a minor documentation placement issue.

Comment on lines +295 to +374
// Scan shared install directories (read-only fallback directories)
for shared_dir in env::MISE_SHARED_INSTALL_DIRS.iter() {
if !shared_dir.is_dir() {
continue;
}
let shared_manifest_path = shared_dir.join(".mise-installs.toml");
let shared_manifest: Manifest = match file::read_to_string(&shared_manifest_path) {
std::result::Result::Ok(body) => toml::from_str(&body).unwrap_or_default(),
Err(_) => Default::default(),
};
let shared_subdirs = match file::dir_subdirs(shared_dir) {
std::result::Result::Ok(d) => d,
Err(err) => {
warn!(
"reading shared install dir {} failed: {err:?}",
display_path(shared_dir)
);
continue;
}
};
for dir_name in shared_subdirs {
let dir = shared_dir.join(&dir_name);
let versions: Vec<String> = file::dir_subdirs(&dir)
.unwrap_or_else(|err| {
warn!("reading versions in {} failed: {err:?}", display_path(&dir));
Default::default()
})
.into_iter()
.filter(|v| !v.starts_with('.'))
.filter(|v| !runtime_symlinks::is_runtime_symlink(&dir.join(v)))
.filter(|v| !dir.join(v).join("incomplete").exists())
.sorted_by_cached_key(|v| {
let normalized = normalize_version_for_sort(v);
(Versioning::new(normalized), v.to_string())
})
.collect();

if versions.is_empty() {
continue;
}

let (short, full, explicit_backend, opts) =
if let Some(mt) = shared_manifest.get(&dir_name) {
(
mt.short.clone(),
mt.full.clone(),
mt.explicit_backend,
mt.opts.clone(),
)
} else {
(dir_name.clone(), None, true, BTreeMap::new())
};

// Merge with existing tool entry or create new one
let tool = tools
.entry(short.clone())
.or_insert_with(|| InstallStateTool {
short: short.clone(),
full: full.clone(),
versions: Vec::new(),
explicit_backend,
opts: opts.clone(),
});
// Add versions from shared dir that aren't already present
for v in versions {
if !tool.versions.contains(&v) {
tool.versions.push(v);
}
}
// Re-sort after merging
tool.versions.sort_by_cached_key(|v| {
let normalized = normalize_version_for_sort(v);
(Versioning::new(normalized), v.to_string())
});
// Fill in metadata if not yet set
if tool.full.is_none() {
tool.full = full;
}
}
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

medium

There's a significant amount of code duplication between the logic for scanning the primary installs directory (lines 201-285) and this new block for scanning shared install directories. The logic for iterating through tool subdirectories, listing versions, and parsing metadata is almost identical in both places.

To improve maintainability and reduce redundancy, consider refactoring this duplicated logic into a helper function. This function could take a directory path and a manifest as input and return the discovered tools and versions, which can then be merged into the main tools map accordingly.

Comment on lines +362 to +375
/// Returns the shared path if found, otherwise the original primary path.
fn resolve_shared_install_path(primary_path: PathBuf, short: &str, pathname: &str) -> PathBuf {
if !primary_path.exists() {
let tool_dir_name = heck::ToKebabCase::to_kebab_case(short);
for shared_dir in env::MISE_SHARED_INSTALL_DIRS.iter() {
let shared_path = shared_dir.join(&tool_dir_name).join(pathname);
if shared_path.exists() {
return shared_path;
}
}
}
primary_path
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

medium

This new function resolve_shared_install_path and its documentation have been inserted between the documentation for version_sub and the function itself. This detaches the doc comment from version_sub, making the code harder to read and potentially causing issues with documentation generation.

Please move this new function and its documentation to a more appropriate location, for example, before the version_sub function's documentation block, to ensure that doc comments are correctly associated with their respective functions.

Comment on lines +131 to +141
// Check shared install directories if the primary path doesn't exist
if !path.exists() && !matches!(&self.request, ToolRequest::Path { .. }) {
let tool_dir_name = heck::ToKebabCase::to_kebab_case(self.ba().short.as_str());
for shared_dir in env::MISE_SHARED_INSTALL_DIRS.iter() {
let shared_path = shared_dir.join(&tool_dir_name).join(&pathname);
if shared_path.exists() {
CACHE.insert(self.clone(), shared_path.clone());
return shared_path;
}
}
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

medium

This block of code for checking shared install directories duplicates logic already present in src/toolset/tool_request.rs within the resolve_shared_install_path function. While this implementation also handles caching, the core logic of iterating through shared directories and constructing paths is the same.

To avoid this duplication, consider making resolve_shared_install_path a public helper function (e.g., in a shared utility module) that can be reused here. You could then handle the caching logic in this file after calling the shared function, based on whether the returned path is different from the primary path.

- Use Settings::try_get() for shared_install_dirs when available,
  falling back to env var. Config file settings now work correctly.
- Add warn! for invalid TOML in shared manifest files (consistency
  with primary manifest error handling).
- Fix misplaced doc comment: restore version_sub docs, separate
  resolve_shared_install_path docs.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
jdx and others added 5 commits March 13, 2026 13:43
…shared flags

- Mark shared_install_dirs setting as [experimental]
- Centralize shared dir lookup into env::find_in_shared_installs(),
  removing duplicate logic from tool_version.rs and tool_request.rs
- Add /usr/local/share/mise/installs as automatic system-level shared
  dir (MISE_SYSTEM_INSTALLS_DIR), always checked when it exists
- Add `mise install --system` to install to the system shared dir
- Add `mise install --shared <path>` to install to a custom shared dir
- Add install_dir field to InstallOptions for path override

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Prevents stale shared-dir paths from persisting after installs/uninstalls
within a single mise process. The CACHE DashMap in ToolVersion::install_path()
is now cleared when install state resets.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…A_DIR

Point to /usr/local/share/mise (the root), not the installs subdir.
Derive installs path as MISE_SYSTEM_DATA_DIR/installs internally,
consistent with how MISE_DATA_DIR relates to MISE_INSTALLS_DIR.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Rename for parity with MISE_SYSTEM_DATA_DIR. MISE_SYSTEM_DIR is still
supported as a legacy fallback in the env var but is no longer
referenced in docs. Internal usage updated to SYSTEM_CONFIG.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@jdx jdx changed the title feat(install): add shared install directories for read-only fallback feat(install): add shared and system install directories Mar 13, 2026
…talls

When installing with --system or --shared, write_backend_meta now
writes the .mise-installs.toml to the target installs directory
instead of skipping the write. Refactored manifest read/write into
path-parameterized helpers.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
jdx and others added 3 commits March 13, 2026 14:10
Test that shared versions are treated as installed (skip re-install),
primary uninstalls don't affect shared versions, and shared versions
remain usable after primary versions are removed.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
mise ls now annotates versions from shared/system install directories
with a label like "(system)" or "(shared)" in cyan, similar to how
"(symlink)" is shown. JSON output also reflects the active state
correctly for shared versions.

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

github-actions bot commented Mar 13, 2026

Hyperfine Performance

xtasks/test/perf

Command mise-2026.3.8 mise Variance
install (cached) 119ms 115ms +3%
ls (cached) 73ms 76ms -3%
bin-paths (cached) 69ms 69ms +0%
task-ls (cached) 726ms 730ms +0%

- Only write .mise-installs.toml for system/shared paths, not
  arbitrary install-into paths
- Use read_manifest_from() in shared dir scan instead of duplicating
- Update Docker docs: explain devcontainer home mount use case for
  --system installs

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Use BackendArg::tool_dir_name() (from installs_path) instead of
  re-applying to_kebab_case on short names in shared dir lookups
- Use std::env::split_paths for MISE_SHARED_INSTALL_DIRS to handle
  Windows paths with colons correctly
- Extract resolve_version_status() to deduplicate shared/system
  detection in mise ls

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

@cursor cursor bot left a comment

Choose a reason for hiding this comment

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

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

Fix All in Cursor

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

When a version exists only in a shared/system dir, `mise install --force`
now installs to the primary dir instead of overwriting the shared copy.
The shared dir's read-only semantics are preserved unless --system or
--shared is explicitly passed.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@jdx jdx merged commit a63eb16 into main Mar 13, 2026
36 of 37 checks passed
@jdx jdx deleted the feat/shared-install-dirs branch March 13, 2026 14:22
mise-en-dev added a commit that referenced this pull request Mar 13, 2026
### 🚀 Features

- **(github)** use release latest endpoint to get latest release by
@roele in [#8516](#8516)
- **(install)** add shared and system install directories by @jdx in
[#8581](#8581)
- **(vfox)** add provenance metadata to lockfile for tool plugins by
@malept in [#8544](#8544)

### 🐛 Bug Fixes

- **(aqua)** expose main binary when files field is empty and
symlink_bins is enabled by @AlexanderTheGrey in
[#8550](#8550)
- **(env)** redact secrets in `mise set` listing and task-specific env
by @jdx in [#8583](#8583)
- **(prepare)** install config tools before running prepare steps by
@jdx in [#8582](#8582)
- **(task)** allow ctrl-c to interrupt tool downloads during `mise run`
by @jdx in [#8571](#8571)
- **(tasks)** add file task header parser support for spaces around = by
@roele in [#8574](#8574)

### 📚 Documentation

- **(task)** add property description for interactive by @roele in
[#8562](#8562)
- add missing `</bold>` closing tag by @muzimuzhi in
[#8564](#8564)
- rebrand site with new chef logo and warm culinary palette by @jdx in
[#8587](#8587)

### 📦️ Dependency Updates

- update ghcr.io/jdx/mise:alpine docker digest to de4657e by
@renovate[bot] in [#8577](#8577)
- update ghcr.io/jdx/mise:copr docker digest to eef29a2 by
@renovate[bot] in [#8578](#8578)
- update ghcr.io/jdx/mise:rpm docker digest to 5a96587 by @renovate[bot]
in [#8580](#8580)
- update ghcr.io/jdx/mise:deb docker digest to 464cf7c by @renovate[bot]
in [#8579](#8579)

### 📦 Registry

- fix flatc version test mismatch by @jdx in
[#8588](#8588)

### Chore

- **(registry)** skip spark test-tool by @jdx in
[#8572](#8572)

### New Contributors

- @AlexanderTheGrey made their first contribution in
[#8550](#8550)

## 📦 Aqua Registry Updates

#### New Packages (6)

- [`bahdotsh/mdterm`](https://github.com/bahdotsh/mdterm)
-
[`callumalpass/mdbase-lsp`](https://github.com/callumalpass/mdbase-lsp)
- [`facebook/ktfmt`](https://github.com/facebook/ktfmt)
- [`gurgeous/tennis`](https://github.com/gurgeous/tennis)
-
[`tektoncd/pipelines-as-code`](https://github.com/tektoncd/pipelines-as-code)
- [`weedonandscott/trolley`](https://github.com/weedonandscott/trolley)

#### Updated Packages (2)

- [`apple/container`](https://github.com/apple/container)
- [`cocogitto/cocogitto`](https://github.com/cocogitto/cocogitto)
fragon10 pushed a commit to fragon10/mise that referenced this pull request Mar 27, 2026
## Summary

Adds support for shared, read-only tool install directories — useful for
Docker containers, toolbox containers, and bastion hosts where a base
set of tools is pre-installed in a shared location.

- **`shared_install_dirs`** setting (`MISE_SHARED_INSTALL_DIRS`,
colon-separated) for additional read-only directories to search for
installed tool versions
- **System install directory** at `/usr/local/share/mise/installs`
(`MISE_SYSTEM_DATA_DIR/installs`) — automatically checked when it
exists, no config needed
- **`mise install --system`** — install tools to the system-wide shared
directory
- **`mise install --shared <path>`** — install tools to a custom shared
directory
- **`mise ls`** — shows `(system)` and `(shared)` labels for versions
from shared directories
- **`MISE_SYSTEM_CONFIG_DIR`** — renamed from `MISE_SYSTEM_DIR` for
parity with `MISE_SYSTEM_DATA_DIR` (legacy env var still supported)
- Feature is marked **[experimental]**

Closes jdx#8549

## How it works

- **Lookup order**: primary install dir → system dir → user-configured
shared dirs
- **Read-only**: shared dirs are never written to during normal `mise
install`; versions found there are treated as already installed
- **Install flags**: `--system` and `--shared` install to alternate
directories and write `.mise-installs.toml` manifests there
- **Config**: works via env var or config file (`shared_install_dirs =
[...]` in settings)
- **Display**: `mise ls` annotates shared versions with `(system)` or
`(shared)` in cyan

## Test plan

- [x] All 531 unit tests pass
- [x] e2e test (`test_shared_install_dirs`) covers:
  - Shared versions appear in `mise ls --installed`
  - `mise x` executes tools from shared dirs
  - `mise install` skips versions already in shared dirs
  - Uninstalling primary versions doesn't affect shared versions
  - Shared versions remain usable after primary removal
- [x] All lint checks pass

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

<!-- CURSOR_SUMMARY -->
---

> [!NOTE]
> **Medium Risk**
> Medium risk because it changes tool install/lookup paths and manifest
handling, which can affect whether versions are considered installed and
where `--force` writes. Also renames the system config env var with a
legacy alias, which could impact deployments relying on the old name.
> 
> **Overview**
> Adds **experimental shared/system install directories** so tool
version resolution can fall back from the primary installs dir to
`/usr/local/share/mise/installs` and any user-configured
`shared_install_dirs` (`MISE_SHARED_INSTALL_DIRS`). Tool path resolution
(`ToolRequest::install_path`, `ToolVersion::install_path`) and
install-state discovery now incorporate these directories, with caching
and manifest reading/writing updated to support per-directory
`.mise-installs.toml`.
> 
> Extends `mise install` with `--system` and `--shared <path>` to
install into alternate directories while ensuring `--force` doesn’t
mutate shared installs unless explicitly targeted, and updates `mise ls`
to label shared/system versions. Renames `MISE_SYSTEM_DIR` to
`MISE_SYSTEM_CONFIG_DIR` (legacy alias retained), updates
schema/settings/docs/manpages/completions, and adds an e2e test covering
shared-dir fallback and force/uninstall behavior.
> 
> <sup>Written by [Cursor
Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit
b70caa7. 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 (1M context) <noreply@anthropic.com>
Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
fragon10 pushed a commit to fragon10/mise that referenced this pull request Mar 27, 2026
### 🚀 Features

- **(github)** use release latest endpoint to get latest release by
@roele in [jdx#8516](jdx#8516)
- **(install)** add shared and system install directories by @jdx in
[jdx#8581](jdx#8581)
- **(vfox)** add provenance metadata to lockfile for tool plugins by
@malept in [jdx#8544](jdx#8544)

### 🐛 Bug Fixes

- **(aqua)** expose main binary when files field is empty and
symlink_bins is enabled by @AlexanderTheGrey in
[jdx#8550](jdx#8550)
- **(env)** redact secrets in `mise set` listing and task-specific env
by @jdx in [jdx#8583](jdx#8583)
- **(prepare)** install config tools before running prepare steps by
@jdx in [jdx#8582](jdx#8582)
- **(task)** allow ctrl-c to interrupt tool downloads during `mise run`
by @jdx in [jdx#8571](jdx#8571)
- **(tasks)** add file task header parser support for spaces around = by
@roele in [jdx#8574](jdx#8574)

### 📚 Documentation

- **(task)** add property description for interactive by @roele in
[jdx#8562](jdx#8562)
- add missing `</bold>` closing tag by @muzimuzhi in
[jdx#8564](jdx#8564)
- rebrand site with new chef logo and warm culinary palette by @jdx in
[jdx#8587](jdx#8587)

### 📦️ Dependency Updates

- update ghcr.io/jdx/mise:alpine docker digest to de4657e by
@renovate[bot] in [jdx#8577](jdx#8577)
- update ghcr.io/jdx/mise:copr docker digest to eef29a2 by
@renovate[bot] in [jdx#8578](jdx#8578)
- update ghcr.io/jdx/mise:rpm docker digest to 5a96587 by @renovate[bot]
in [jdx#8580](jdx#8580)
- update ghcr.io/jdx/mise:deb docker digest to 464cf7c by @renovate[bot]
in [jdx#8579](jdx#8579)

### 📦 Registry

- fix flatc version test mismatch by @jdx in
[jdx#8588](jdx#8588)

### Chore

- **(registry)** skip spark test-tool by @jdx in
[jdx#8572](jdx#8572)

### New Contributors

- @AlexanderTheGrey made their first contribution in
[jdx#8550](jdx#8550)

## 📦 Aqua Registry Updates

#### New Packages (6)

- [`bahdotsh/mdterm`](https://github.com/bahdotsh/mdterm)
-
[`callumalpass/mdbase-lsp`](https://github.com/callumalpass/mdbase-lsp)
- [`facebook/ktfmt`](https://github.com/facebook/ktfmt)
- [`gurgeous/tennis`](https://github.com/gurgeous/tennis)
-
[`tektoncd/pipelines-as-code`](https://github.com/tektoncd/pipelines-as-code)
- [`weedonandscott/trolley`](https://github.com/weedonandscott/trolley)

#### Updated Packages (2)

- [`apple/container`](https://github.com/apple/container)
- [`cocogitto/cocogitto`](https://github.com/cocogitto/cocogitto)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant