feat(env): add environment caching with module cacheability support#7761
Conversation
There was a problem hiding this comment.
Pull request overview
This PR adds environment caching to mise to improve performance for nested invocations, using session-scoped encrypted storage and comprehensive cache invalidation mechanisms.
Changes:
- Implements encrypted environment caching system with ChaCha20-Poly1305 encryption and session-scoped keys
- Extends vfox plugin system to support module cacheability metadata via
MiseEnvResultstructure - Adds
--fresh-env/-Fflags toexecandruncommands to bypass cache on demand
Reviewed changes
Copilot reviewed 20 out of 21 changed files in this pull request and generated 6 comments.
Show a summary per file
| File | Description |
|---|---|
| src/toolset/env_cache.rs | New module implementing core cache system with encryption, TTL, and watch file validation |
| src/toolset/toolset_env.rs | Integrates cache loading/saving logic into environment computation |
| src/plugins/vfox_plugin.rs | Updates mise_env to return MiseEnvResponse with cache metadata |
| crates/vfox/src/hooks/mise_env.rs | Implements MiseEnvResult with backward-compatible legacy format support |
| src/cli/exec.rs | Adds --fresh-env flag and cache key propagation to exec command |
| src/cli/run.rs | Adds --fresh-env flag to run command |
| src/cli/activate.rs | Generates and exports encryption key during shell activation |
| src/config/env_directive/module.rs | Tracks module cacheability and watch files in environment results |
| settings.toml | Defines env_cache and env_cache_ttl configuration settings |
| e2e/env/test_env_cache | Adds end-to-end tests for basic caching functionality |
| e2e/env/test_env_cache_fresh | Adds tests for --fresh-env flag behavior |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| let base_path = env::PATH | ||
| .iter() | ||
| .map(|p| p.to_string_lossy()) | ||
| .collect::<Vec<_>>() | ||
| .join(":"); |
There was a problem hiding this comment.
The path separator is hardcoded as : which is Unix-specific. This will not work correctly on Windows where the separator is ;. Use std::path::MAIN_SEPARATOR or a cross-platform approach to join paths.
| let key: [u8; 32] = rand::random(); | ||
| BASE64_STANDARD.encode(key) |
There was a problem hiding this comment.
Using rand::random() for cryptographic key generation is not cryptographically secure. Use ChaCha20Poly1305::generate_key(&mut OsRng) or a CSPRNG like rand::rngs::OsRng to ensure secure random key generation.
| let key: [u8; 32] = rand::random(); | |
| BASE64_STANDARD.encode(key) | |
| let key = ChaCha20Poly1305::generate_key(&mut OsRng); | |
| BASE64_STANDARD.encode(&key) |
| // SAFETY: We're only unsetting a mise-specific env var, not affecting other | ||
| // processes or causing data races | ||
| unsafe { | ||
| std::env::remove_var("__MISE_ENV_CACHE_KEY"); | ||
| } |
There was a problem hiding this comment.
Using unsafe to modify environment variables is unnecessary here. While the comment explains the reasoning, std::env::remove_var is safe in Rust and doesn't require an unsafe block. Remove the unsafe wrapper.
| // SAFETY: We're only unsetting a mise-specific env var, not affecting other | |
| // processes or causing data races | |
| unsafe { | |
| std::env::remove_var("__MISE_ENV_CACHE_KEY"); | |
| } | |
| // We're only unsetting a mise-specific env var, not affecting other | |
| // processes or causing data races | |
| std::env::remove_var("__MISE_ENV_CACHE_KEY"); |
| // SAFETY: We're only unsetting a mise-specific env var, not affecting other | ||
| // processes or causing data races | ||
| unsafe { | ||
| std::env::remove_var("__MISE_ENV_CACHE_KEY"); | ||
| } |
There was a problem hiding this comment.
Using unsafe to modify environment variables is unnecessary here. While the comment explains the reasoning, std::env::remove_var is safe in Rust and doesn't require an unsafe block. Remove the unsafe wrapper.
| // SAFETY: We're only unsetting a mise-specific env var, not affecting other | |
| // processes or causing data races | |
| unsafe { | |
| std::env::remove_var("__MISE_ENV_CACHE_KEY"); | |
| } | |
| std::env::remove_var("__MISE_ENV_CACHE_KEY"); |
| } else { | ||
| // Legacy format: table is actually an array of env keys | ||
| // Try to parse as array | ||
| let env: Vec<EnvKey> = Vec::from_lua(Value::Table(table), lua)?; |
There was a problem hiding this comment.
If parsing as an array fails, the error message won't clearly indicate this is a legacy format issue. Consider wrapping this with a more descriptive error message using map_err to help users understand the format requirements.
| let env: Vec<EnvKey> = Vec::from_lua(Value::Table(table), lua)?; | |
| let env: Vec<EnvKey> = Vec::from_lua(Value::Table(table), lua).map_err(|e| { | |
| LuaError::RuntimeError(format!( | |
| "Failed to parse legacy MiseEnv hook result as an array of env keys. \ | |
| Expected an array like {{ {{\"KEY\", \"VALUE\"}}, ... }}. Underlying error: {e}" | |
| )) | |
| })?; |
| sha1 = "0.10" | ||
| sha2 = "0.10" | ||
| blake3 = "1" | ||
| chacha20poly1305 = "0.10" |
There was a problem hiding this comment.
The version 0.10 of chacha20poly1305 may not be the latest. Consider using a more recent version to benefit from security updates and improvements. Check crates.io for the latest stable version.
| chacha20poly1305 = "0.10" | |
| chacha20poly1305 = "0.10.1" |
Hyperfine Performance
|
| Command | Mean [ms] | Min [ms] | Max [ms] | Relative |
|---|---|---|---|---|
mise-2026.1.5 x -- echo |
15.1 ± 0.4 | 14.1 | 16.4 | 1.00 |
mise x -- echo |
15.3 ± 0.4 | 14.3 | 17.3 | 1.01 ± 0.04 |
mise env
| Command | Mean [ms] | Min [ms] | Max [ms] | Relative |
|---|---|---|---|---|
mise-2026.1.5 env |
15.9 ± 0.6 | 14.0 | 17.7 | 1.00 |
mise env |
16.6 ± 0.6 | 15.5 | 22.1 | 1.05 ± 0.05 |
mise hook-env
| Command | Mean [ms] | Min [ms] | Max [ms] | Relative |
|---|---|---|---|---|
mise-2026.1.5 hook-env |
16.1 ± 0.4 | 15.1 | 17.3 | 1.00 ± 0.04 |
mise hook-env |
16.1 ± 0.5 | 15.0 | 18.8 | 1.00 |
mise ls
| Command | Mean [ms] | Min [ms] | Max [ms] | Relative |
|---|---|---|---|---|
mise-2026.1.5 ls |
14.2 ± 0.4 | 13.2 | 15.8 | 1.00 |
mise ls |
14.2 ± 0.4 | 13.2 | 15.3 | 1.00 ± 0.04 |
xtasks/test/perf
| Command | mise-2026.1.5 | mise | Variance |
|---|---|---|---|
| install (cached) | 82ms | 82ms | +0% |
| ls (cached) | 54ms | 54ms | +0% |
| bin-paths (cached) | 55ms | 55ms | +0% |
| task-ls (cached) | 232ms | 233ms | +0% |
Implements environment caching for mise with encrypted session-scoped storage: - Add env_cache and env_cache_ttl settings to enable/configure caching - Create src/toolset/env_cache.rs with ChaCha20-Poly1305 encryption - Cache keys based on config files, tool versions, settings, and base PATH - Extend vfox MiseEnvResult with cacheable and watch_files fields - Update module.rs to handle cache metadata from vfox plugins - Add --fresh-env/-F flag to exec and run commands to bypass cache - Generate session-scoped encryption key on activate - Propagate cache key to subprocesses for nested invocations - Add e2e tests for env caching The cache is encrypted with a session-scoped key that is: - Generated on `mise activate` and exported as __MISE_ENV_CACHE_KEY - Lost when the shell session ends (provides security) - Propagated to child processes for nested mise invocations Cache invalidates when: - Config file mtimes change - Tool versions change - Settings change - mise version changes - TTL expires (default 1h) - Module watch_files change Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Remove -F short alias from --fresh-env flag (fixes sorted subcommands test) - Add reset_env_cache_key() helper in env.rs for safe env var removal - Use std::env::join_paths for platform-appropriate PATH separator - Add #[allow(dead_code)] to clear/clear_stale functions (not yet used) - Update e2e test comments to remove -F references Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
The clap-sort library requires long-only flags to be in alphabetical order. Move --fresh-env before --no-prepare in both commands. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
d77c1b1 to
4102d69
Compare
- Remove unused clear_stale() function from env_cache.rs - Add env cache clearing to `mise cache clear` - Add env cache pruning to `mise cache prune` (uses env_cache_ttl) - Add env cache pruning to auto_prune() (uses env_cache_ttl) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Use CSPRNG (ChaCha20Poly1305::generate_key) for key generation - Use saturating_sub in TTL validation to prevent integer underflow - Improve error message for MiseEnv hook parsing failures Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Add documentation for the extended MiseEnv return format with cacheable and watch_files options for env plugin development. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Add plugin directory to watch_files for cache invalidation This ensures env cache invalidates when plugin is updated via `mise plugins update` or manual edit - Fix silent type error swallowing in MiseEnvResult parsing Now properly reports type errors for env, cacheable, and watch_files fields instead of silently using defaults Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Absolutize relative watch_files paths in module.rs Ensures consistent cache validation regardless of which directory mise is run from - Use ensure_encryption_key() in task executor for consistent behavior Now both `mise exec` and `mise run` enable caching for subprocesses Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 1 potential issue.
Bugbot Autofix is OFF. To automatically fix reported issues with Cloud Agents, enable Autofix in the Cursor dashboard.
|
|
||
| // Add plugin directory to watch files for cache invalidation | ||
| // This ensures cache invalidates when plugin is updated | ||
| r.watch_files.push(path); |
There was a problem hiding this comment.
Plugin directory mtime doesn't detect file modifications
Medium Severity
The plugin directory path is added to watch_files for cache invalidation, but directory mtimes only change when files are added or removed, not when existing files are modified. This means updating a plugin's source code (like editing hooks/mise_env.lua) won't invalidate the cache, causing stale cached environment values to persist. The comment claims this "ensures cache invalidates when plugin is updated" but that's incorrect for file modifications.
### 🚀 Features - **(config)** add miette diagnostics for TOML parsing errors by @jdx in [#7764](#7764) - **(env)** add environment caching with module cacheability support by @jdx in [#7761](#7761) ### 🐛 Bug Fixes - **(prepare)** handle freshness check for auto-created venvs by @jdx in [#7770](#7770) - **(release)** use colon separator in release titles by @jdx in [#7765](#7765) - **(release)** drop Fedora 41 from COPR build (EOL) by @TobiX in [#7771](#7771) ### 🚜 Refactor - improve filetask field parsing tests and parser by @makp0 in [#7751](#7751) ### 📚 Documentation - improve CLAUDE.md with additional development guidance by @jdx in [#7763](#7763) - drop architecture from Debian sources.list by @TobiX in [#7772](#7772) ### 📦 Registry - use aqua for zprint by @scop in [#7767](#7767) ### Security - remove insecure registry-comment workflow by @jdx in [#7769](#7769) ## 📦 Aqua Registry Updates #### New Packages (2) - [`cameron-martin/bazel-lsp`](https://github.com/cameron-martin/bazel-lsp) - [`micro-editor/micro`](https://github.com/micro-editor/micro)
…caching Mise v2025.5.6 includes built-in environment caching for 'mise activate' that provides module-level cacheability for faster shell startup times. See: jdx/mise#7761 Removed: - _mise_evalcache() and _mise_evalcache_clear() functions from 17-mise.sh - bash:clear-mise-cache task reference from mise:update task - Custom mise caching documentation from bash.md The previous custom solution cached 'mise activate bash' output to ~/.cache/dotfiles/bash/mise/activate_bash.sh with manual invalidation based on mise binary and installs directory timestamps. The first-party solution is more robust as it handles environment resolution caching at the module level, particularly benefiting projects with many tools or complex configurations. Note: General purpose _evalcache for other tools (zoxide, atuin, etc.) remains unchanged in 05-cache.sh.
Summary
cacheableandwatch_filesfields for module cacheability--fresh-env/-Fflag to exec and run commands to bypass cacheImplementation Details
Cache System
~/.local/state/mise/env-cache/mise activateand exported as__MISE_ENV_CACHE_KEYCache Invalidation
Cache invalidates when:
env_cache_ttl)watch_fileschangeNew Settings
Module Cacheability
vfox plugins can now return cache metadata:
Legacy format (array of env vars) still works and is treated as uncacheable for backward compatibility.
Test plan
mise run test:e2e test_env_cache- Basic caching testsmise run test:e2e test_env_cache_fresh- Tests for --fresh-env flagMISE_ENV_CACHE=1in shell🤖 Generated with Claude Code
Note
Introduces an encrypted, session-scoped environment cache and integrates it across env resolution, CLI, and settings.
~/.local/state/mise/env-cachewith TTL (env_cache_ttl), key via__MISE_ENV_CACHE_KEYgenerated onactivate; cache load/save, watch-file mtime validation, and invalidation on config/tool/settings/version changesMiseEnvto return{env, cacheable, watch_files}; consumers updated to track cacheability and watch files--fresh-envtoexec/run(and tasks/shims propagation) to bypass cacheenv_cacheandenv_cache_ttl(schema +settings.toml), and includes env cache incache prune/cache clear--fresh-envhexandchacha20poly1305Written by Cursor Bugbot for commit 77b1d29. This will update automatically on new commits. Configure here.