docs/backend-plugin-development.md
::: tip The mise-backend-plugin-template provides a ready-to-use starting point with LuaCATS type definitions, stylua formatting, and hk linting pre-configured. :::
Backend plugins in mise use dedicated backend hooks to manage multiple tools with the plugin:tool format. They are well suited to package managers, tool families, and custom installations that manage several related tools.
Backend plugins extend the standard vfox plugin system with dedicated backend hooks. They support:
vfox-npm can install prettier, eslint, and other npm packagesBackend plugins are generally a git repository but can also be a directory (via mise plugin link).
Backend plugins are written in Lua (currently version 5.1). They use three main backend methods, each implemented in its own file:
hooks/backend_list_versions.lua - Lists available versions for a toolhooks/backend_install.lua - Installs a specific version of a toolhooks/backend_exec_env.lua - Sets up environment variables for a toolLists available versions for a tool:
function PLUGIN:BackendListVersions(ctx)
local tool = ctx.tool
local options = ctx.options
local versions = {}
-- Your logic to fetch versions for the tool
-- Example: query an API, parse a registry, etc.
-- Access custom options via options["key"] or options.key
return {versions = versions}
end
[!WARNING] Return versions oldest to newest, according to the tool's release policy. mise preserves that order. Do not assume SemVer: versions may be dates, prereleases, or channel names. This is the opposite of a tool plugin's
Availablehook, which returns newest first.
Installs a specific version of a tool:
function PLUGIN:BackendInstall(ctx)
local tool = ctx.tool
local version = ctx.version
local install_path = ctx.install_path
local download_path = ctx.download_path
local options = ctx.options
-- Your logic to install the tool
-- Example: download files, extract archives, etc.
-- Access custom options via options["key"] or options.key
return {}
end
Returns environment entries for a selected installation. Implement this hook even when
there are no entries to add; return {env_vars = {}} in that case:
function PLUGIN:BackendExecEnv(ctx)
local install_path = ctx.install_path
local options = ctx.options
-- Your logic to set up environment variables
-- Example: add bin directories to PATH
-- Access custom options via options["key"] or options.key
return {
env_vars = {
{key = "PATH", value = install_path .. "/bin"}
}
}
end
Use the dedicated mise-backend-plugin-template to create backend plugins:
# Option 1: Use GitHub's template feature (recommended)
# Visit https://github.com/jdx/mise-backend-plugin-template
# Click "Use this template" to create your repository
# Option 2: Clone and modify
git clone https://github.com/jdx/mise-backend-plugin-template my-backend-plugin
cd my-backend-plugin
rm -rf .git
git init
The template includes:
Create a directory with this structure:
my-backend-plugin/
├── metadata.lua # Plugin metadata
├── hooks/
│ ├── backend_list_versions.lua # BackendListVersions hook
│ ├── backend_install.lua # BackendInstall hook
│ └── backend_exec_env.lua # BackendExecEnv hook
PLUGIN = {
name = "vfox-npm",
version = "1.0.0",
description = "Backend plugin for npm packages",
author = "Your Name"
}
This small teaching implementation uses npm to install packages. It requires a POSIX shell and Node/npm on PATH; the commands below are not a Windows implementation. For everyday use, prefer the built-in npm backend, which handles platform integration and additional installation options.
The snippets belong in the three hook files shown. They pass package values through quoted environment variables instead of concatenating them into shell commands.
PLUGIN = {
name = "vfox-npm",
version = "1.0.0",
description = "Backend plugin for npm packages",
author = "Plugin Author",
depends = { "node" },
}
function PLUGIN:BackendListVersions(ctx)
if RUNTIME.osType == "windows" then
error("This example requires a POSIX shell")
end
local cmd = require("cmd")
local json = require("json")
local result = cmd.exec('npm view "$MISE_PLUGIN_PACKAGE" versions --json', {
env = {MISE_PLUGIN_PACKAGE = ctx.tool},
})
local versions = json.decode(result)
-- npm can return a single version as a string.
if type(versions) == "string" then
versions = {versions}
end
if type(versions) ~= "table" or #versions == 0 then
error("No versions returned for " .. ctx.tool)
end
return {versions = versions}
end
function PLUGIN:BackendInstall(ctx)
if RUNTIME.osType == "windows" then
error("This example requires a POSIX shell")
end
local cmd = require("cmd")
cmd.exec('npm install --no-package-lock --no-save -- "$MISE_PLUGIN_SPEC"', {
cwd = ctx.install_path,
env = {MISE_PLUGIN_SPEC = ctx.tool .. "@" .. ctx.version},
})
return {}
end
function PLUGIN:BackendExecEnv(ctx)
local file = require("file")
return {
env_vars = {
{key = "PATH", value = file.join_path(ctx.install_path, "node_modules", ".bin")}
}
}
end
The plugin name doesn't have to match the repository name. The backend prefix is whatever name the plugin was installed under.
# Link the example you created and configure its prerequisite
mise plugin link vfox-npm /path/to/your/plugin
mise use node@24
# List available versions
mise ls-remote vfox-npm:prettier
# Install a specific version
mise install vfox-npm:[email protected]
# Use in a project
mise use vfox-npm:prettier@latest
# Execute the tool
mise exec -- prettier --help
Use a name that does not collide with a built-in backend. To test different registries or
behavior, define explicit tool options and read ctx.options; the installed name is not a
substitute for an options contract.
Backend plugins receive context through the ctx parameter passed to each hook function:
| Variable | Description | Example |
|---|---|---|
ctx.tool | The tool name | "prettier" |
ctx.options | Tool options from mise.toml | {channels = {"a", "b"}} |
| Variable | Description | Example |
|---|---|---|
ctx.tool | The tool name | "prettier" |
ctx.version | The requested version | "3.0.0" |
ctx.install_path | Installation directory | "/home/user/.local/share/mise/installs/vfox-npm-prettier/3.0.0" |
ctx.download_path | Download directory | "/home/user/.local/share/mise/downloads/vfox-npm-prettier/3.0.0" |
ctx.options | Tool options from mise.toml | {exe = "rg"} |
| Variable | Description | Example |
|---|---|---|
ctx.tool | The tool name | "prettier" |
ctx.version | The requested version | "3.0.0" |
ctx.install_path | Installation directory | "/home/user/.local/share/mise/installs/vfox-npm-prettier/3.0.0" |
ctx.options | Tool options from mise.toml | {exe = "rg"} |
[!TIP] Option values preserve their TOML types as native Lua equivalents. Strings remain strings, arrays become Lua sequence tables, and nested tables become Lua map tables. For example,
channels = ["conda-forge", "robostack"]inmise.tomlbecomes a Lua table you can iterate withipairs(ctx.options.channels).
# Link your plugin for development
mise plugin link my-plugin /path/to/my-plugin
# Test listing versions
mise ls-remote my-plugin:some-tool
# Test installation
mise use my-plugin:[email protected]
# Test execution
mise exec -- some-tool --version
Use debug mode to see detailed plugin execution:
mise --debug install my-plugin:[email protected]
cmd.exec raises an error on a nonzero exit status and includes stderr. Do not hide stderr
or search successful stdout for an error string. Check HTTP status codes before parsing
bodies, validate required response fields, and keep credentials out of errors.
The Lua modules reference explains synchronous errors and the
HTTP try_* methods for recoverable transport failures.
Parse versions with Lua patterns (Lua does not have regular expressions; string.match/string.gsub use Lua's own pattern syntax):
local function parse_version(version_string)
-- Remove prefixes like 'v' or 'release-'
return version_string:gsub("^v", ""):gsub("^release%-", "")
end
Use file.join_path for path construction and cmd.exec's cwd option for the command's
working directory. Prefer file operations over shelling out to mkdir, cp, or mv.
If your installer needs shell commands, document the shell and quote every external value.
local file = require("file")
local bin_path = file.join_path(ctx.install_path, "bin")
The Lua runtime does not translate a shell command between operating systems. A POSIX
mkdir -p, $VARIABLE, or chmod example needs a different implementation on Windows.
Test on each platform you claim to support, including paths containing spaces.
Choose installation logic using ctx.tool, ctx.version, and RUNTIME. Validate that the
tool and platform are supported before downloading or running an installer. Keep shared
logic in a Lua helper module instead of duplicating the same command in every branch.
vfox automatically injects runtime information into your plugin:
function PLUGIN:BackendInstall(ctx)
-- Platform-specific installation using injected RUNTIME object
if RUNTIME.osType == "darwin" then
-- macOS installation logic
elseif RUNTIME.osType == "linux" then
-- Linux installation logic
elseif RUNTIME.osType == "windows" then
-- Windows installation logic
end
return {}
end
The RUNTIME object provides:
RUNTIME.osType: Operating system type ("windows", "linux", "darwin")RUNTIME.archType: Architecture ("amd64", "arm64", "x86", etc.)RUNTIME.envType: libc environment type ("gnu" on glibc Linux, "musl" on musl Linux, nil on Windows/macOS and undetected systems)RUNTIME.version: vfox runtime versionRUNTIME.pluginDirPath: Plugin directory pathSet multiple environment variables:
function PLUGIN:BackendExecEnv(ctx)
-- Add node_modules/.bin to PATH for npm-installed binaries
local bin_path = ctx.install_path .. "/node_modules/.bin"
return {
env_vars = {
{key = "PATH", value = bin_path},
{key = "EXAMPLE_TOOL_HOME", value = ctx.install_path},
{key = "EXAMPLE_TOOL_VERSION", value = ctx.version}
}
}
end
mise caches remote version lists and tool environment results. During development, use
mise cache clear my-plugin:some-tool when a cached result hides a hook change. A Lua table
only caches within that Lua runtime; it does not persist across separate mise invocations.
See cache behavior and the Lua modules reference.