docs/env-plugin-development.md
Environment plugins are a special type of mise plugin that provide environment variables and PATH modifications without managing tool versions. They're ideal for integrating external services, managing secrets, and standardizing environment configuration across teams.
Unlike tool plugins and backend plugins, environment plugins:
Available, PreInstall, PostInstall hooks)MiseEnv, MisePath)env._.<plugin-name> syntaxThe fastest way to create an environment plugin is to use the mise-env-plugin-template.
::: tip The mise-env-plugin-template provides a ready-to-use starting point with LuaCATS type definitions, stylua formatting, and hk linting pre-configured. :::
To get started:
# Clone the template
git clone https://github.com/jdx/mise-env-plugin-template my-env-plugin
cd my-env-plugin
# Customize for your use case
# Edit metadata.lua, hooks/mise_env.lua, hooks/mise_path.lua
Environment plugins are implemented in Lua (version 5.1 at the moment). A minimal environment plugin has this structure:
my-env-plugin/
├── metadata.lua # Plugin metadata
└── hooks/
├── mise_env.lua # Returns environment variables (required)
└── mise_path.lua # Returns PATH entries (optional)
The metadata.lua file defines your plugin's basic information:
PLUGIN = {}
--- Plugin name (required)
PLUGIN.name = "my-env-plugin"
--- Plugin version (required)
PLUGIN.version = "1.0.0"
--- Plugin description (required)
PLUGIN.description = "Provides environment variables for my service"
--- Plugin homepage (optional)
PLUGIN.homepage = "https://github.com/username/my-env-plugin"
--- Plugin license (optional)
PLUGIN.license = "MIT"
--- Minimum mise/vfox version required (optional)
PLUGIN.minRuntimeVersion = "0.3.0"
The MiseEnv hook returns environment variables to set:
function PLUGIN:MiseEnv(ctx)
-- Access configuration from mise.toml via ctx.options
local api_url = ctx.options.api_url or "https://api.example.com"
local debug = ctx.options.debug or false
-- Return array of environment variables
return {
{
key = "API_URL",
value = api_url
},
{
key = "DEBUG",
value = tostring(debug)
},
{
key = "SERVICE_TOKEN",
value = get_token_from_somewhere() -- Your custom logic
}
}
end
::: tip
When cmd.exec() is called from MiseEnv or MisePath hooks, it inherits the mise-constructed environment — including _.path entries and environment variables from preceding directives. If the module directive is configured with tools = true (e.g., _.my-plugin = { tools = true }), tool installation bin paths are also included, so mise-managed tools are directly callable (e.g., cmd.exec("node --version")).
:::
Return value: Either a simple array of env keys, or a table with caching metadata.
Simple format - array of tables, each with:
key (string, required): Environment variable namevalue (string, required): Environment variable valueExtended format - table with:
env (array, required): Array of {key, value} tables (same as simple format)cacheable (boolean, optional): If true, mise can cache this plugin's output. Default: falsewatch_files (array of strings, optional): File paths to watch for changes. If any file's mtime changes, the cache is invalidated.Example using extended format with caching:
function PLUGIN:MiseEnv(ctx)
local config_path = ctx.options.config_file or "config.json"
local config = load_config(config_path)
return {
cacheable = true,
watch_files = {config_path},
env = {
{key = "API_URL", value = config.api_url},
{key = "API_KEY", value = config.api_key}
}
}
end
When cacheable = true, mise will cache the environment variables and only re-execute the plugin when:
watch_files changesenv_cache_ttl setting)::: tip
For caching to work, users must enable the env_cache setting:
# ~/.config/mise/config.toml
[settings]
env_cache = true
:::
The MisePath hook returns directories to add to PATH (optional):
function PLUGIN:MisePath(ctx)
-- Return array of paths to prepend to PATH
local paths = {
"/opt/my-service/bin"
}
-- Optionally add user-configured path
if ctx.options.custom_bin_path then
table.insert(paths, ctx.options.custom_bin_path)
end
return paths
end
Return value: Array of strings (directory paths)
Both hooks receive a ctx parameter with:
ctx.options: TOML table of user configuration from mise.tomlFor environment plugins, ctx.options is the primary way to accept user configuration.
Users configure environment plugins using the env._ directive:
Simple activation with no options:
[env]
_.my-env-plugin = {}
With configuration options:
[env]
_.my-env-plugin = {
api_url = "https://prod.api.example.com",
debug = false,
custom_bin_path = "/custom/path/bin",
}
All fields in the TOML table are passed to your hooks as ctx.options.
Here's a complete example of a plugin that fetches secrets from an external service:
metadata.lua:
PLUGIN = {}
PLUGIN.name = "vault-secrets"
PLUGIN.version = "1.0.0"
PLUGIN.description = "Fetch secrets from HashiCorp Vault"
PLUGIN.minRuntimeVersion = "0.3.0"
hooks/mise_env.lua:
local http = require("http")
local json = require("json")
function PLUGIN:MiseEnv(ctx)
local vault_url = ctx.options.vault_url or error("vault_url required")
local secrets_path = ctx.options.secrets_path or error("secrets_path required")
local vault_token = os.getenv("VAULT_TOKEN") or error("VAULT_TOKEN not set")
-- Fetch secrets from Vault
local url = vault_url .. "/v1/" .. secrets_path
local response = http.get({
url = url,
headers = {
["X-Vault-Token"] = vault_token
}
})
if response.status_code ~= 200 then
error("Failed to fetch secrets: " .. response.status_code)
end
local data = json.decode(response.body)
local env_vars = {}
-- Convert Vault secrets to environment variables
for key, value in pairs(data.data.data) do
table.insert(env_vars, {
key = key,
value = value
})
end
return env_vars
end
Usage in mise.toml:
[env]
_.vault-secrets = {
vault_url = "https://vault.example.com",
secrets_path = "secret/data/myapp/production",
}
Environment plugins have access to mise's built-in Lua modules:
http: Make HTTP requestsjson: Encode/decode JSONfile: Read/write filescmd: Execute shell commandsstrings: String manipulation utilitiesenv: Access environment variablesSee Plugin Lua Modules for complete documentation.
function PLUGIN:MiseEnv(ctx)
local api_url = ctx.options.api_url or "https://api.example.com"
local timeout = ctx.options.timeout or 30
-- ...
end
function PLUGIN:MiseEnv(ctx)
if not ctx.options.api_key then
error("api_key is required in mise.toml configuration")
end
-- ...
end
function PLUGIN:MiseEnv(ctx)
local response = http.get({url = ctx.options.api_url})
if response.status_code ~= 200 then
error("API request failed: " .. response.status_code .. " - " .. response.body)
end
-- ...
end
For plugins that fetch data from external services, use mise's built-in caching by returning the extended format with cacheable = true:
function PLUGIN:MiseEnv(ctx)
local config_file = ctx.options.config_file or "secrets.json"
-- Fetch secrets (mise will cache the result)
local secrets = fetch_secrets(ctx.options)
return {
cacheable = true,
watch_files = {config_file}, -- Re-fetch if config changes
env = secrets
}
end
This is preferred over manual caching because:
mise cache clear and mise cache pruneenv_cache_ttl settingNote: Users must enable env_cache = true in their settings for caching to work.
function PLUGIN:MiseEnv(ctx)
local env_name = ctx.options.environment or "development"
-- Load different config based on environment
local config = load_config(env_name)
return {
{key = "ENV", value = env_name},
{key = "API_URL", value = config.api_url},
-- ...
}
end
mise plugin link my-env-plugin /path/to/my-env-plugin
mise.toml:[env]
_.my-env-plugin = { test_option = "value" }
# See environment variables
mise env | grep MY_
# Run a command with the environment
mise exec -- env | grep MY_
# Debug with MISE_DEBUG
MISE_DEBUG=1 mise env
Plugin not found: Make sure you've installed/linked the plugin:
mise plugin ls
Hook not executing: Enable debug logging:
MISE_DEBUG=1 mise env
Options not passed: Verify TOML syntax in mise.toml:
[env]
# Correct: TOML table
_.my-plugin = { key = "value" }
# Wrong: String value
_.my-plugin = "value" # This won't work
Once your environment plugin is ready:
mise plugin install.See Plugin Publishing for detailed instructions.
If you have an existing tool plugin that only sets environment variables, you can simplify it to an environment-only plugin:
Before (tool plugin with unused hooks):
my-plugin/
├── metadata.lua
└── hooks/
├── available.lua # Returns empty list
├── pre_install.lua # Not used
├── post_install.lua # Not used
└── env_keys.lua # Actually sets env vars
After (environment plugin):
my-plugin/
├── metadata.lua
└── hooks/
└── mise_env.lua # Clean and focused