Skip to content

Backend plugins ​

A backend plugin is a set of Lua hooks that installs many tools under one prefix. Users write my-backend:some-tool, where my-backend is the name they installed the plugin under. Write one for a package registry or an artifact server that no built-in backend supports.

Backend plugins are Lua 5.1 scripts in a hooks/ directory, like tool plugins, but with their own hooks. A plugin directory that has hooks/backend_install.lua is a backend plugin.

Quick start ​

Create a repository from the backend plugin template, either with Use this template on GitHub or with the GitHub CLI:

sh
gh repo create mise-acme --template jdx/mise-backend-plugin-template --public --clone

The template includes LuaCATS type definitions for editor completion, and runs stylua, lua-language-server and actionlint through hk. A backend plugin has this layout:

text
mise-acme/
├── metadata.lua                  # name and version
├── hooks/
│   ├── backend_list_versions.lua # required
│   ├── backend_install.lua       # required
│   ├── backend_exec_env.lua      # required
│   ├── backend_uninstall.lua     # optional
│   ├── backend_list_tools.lua    # optional
│   └── backend_search_tools.lua  # optional
└── lib/                          # optional: modules for require()

Link your working copy and try a tool:

sh
mise plugins link acme ./mise-acme
mise ls-remote acme:deploy
mise use acme:deploy@2.1.0
mise exec -- deploy --version

The prefix is the name you linked or installed the plugin under. Pick one that does not clash with a built-in backend such as npm or github.

Complete example ​

This plugin installs tools from an internal artifact server that lists each tool's versions, oldest first, in index.json and puts each release's archives and a SHA256SUMS file in a directory named after the version:

text
https://artifacts.example.com/deploy/index.json
https://artifacts.example.com/deploy/2.1.0/deploy-2.1.0-linux-x64.tar.gz
https://artifacts.example.com/deploy/2.1.0/SHA256SUMS

A mirror tool option replaces the server URL.

metadata.lua
lua
PLUGIN = {
    name = "acme",
    version = "0.1.0",
    description = "Install tools from the ACME artifact server",
}
lib/acme.lua
lua
local M = {}

function M.base_url(options)
    return options.mirror or "https://artifacts.example.com"
end

-- The server builds for Linux and macOS only.
function M.asset_name(tool, version)
    local os_names = { darwin = "macos", linux = "linux" }
    local arch_names = { amd64 = "x64", arm64 = "arm64" }
    local os_name = os_names[RUNTIME.osType] or error("unsupported OS: " .. RUNTIME.osType)
    local arch = arch_names[RUNTIME.archType] or error("unsupported architecture: " .. RUNTIME.archType)
    return tool .. "-" .. version .. "-" .. os_name .. "-" .. arch .. ".tar.gz"
end

function M.find_checksum(body, filename)
    for line in body:gmatch("[^\r\n]+") do
        local digest, name = line:match("^(%x+)%s+%*?(.+)$")
        if name == filename and #digest == 64 then
            return digest
        end
    end
    error("no SHA-256 for " .. filename)
end

-- The Lua modules have no hash function, so ask the system for one.
function M.sha256(path)
    local cmd = require("cmd")
    local command = RUNTIME.osType == "darwin" and 'shasum -a 256 "$FILE"' or 'sha256sum "$FILE"'
    return cmd.exec(command, { env = { FILE = path } }):match("^(%x+)")
end

return M
hooks/backend_list_versions.lua
lua
local http = require("http")
local json = require("json")
local acme = require("acme")

function PLUGIN:BackendListVersions(ctx)
    local url = acme.base_url(ctx.options) .. "/" .. ctx.tool .. "/index.json"
    local resp = http.get({ url = url })
    if resp.status_code == 404 then
        error("acme has no tool named " .. ctx.tool)
    elseif resp.status_code ~= 200 then
        error("fetching " .. url .. " failed: HTTP " .. resp.status_code)
    end
    -- index.json lists versions oldest first, the order mise expects
    return { versions = json.decode(resp.body).versions }
end
hooks/backend_install.lua
lua
local archiver = require("archiver")
local file = require("file")
local http = require("http")
local acme = require("acme")

function PLUGIN:BackendInstall(ctx)
    local dir = acme.base_url(ctx.options) .. "/" .. ctx.tool .. "/" .. ctx.version .. "/"
    local filename = acme.asset_name(ctx.tool, ctx.version)
    local archive = file.join_path(ctx.download_path, filename)

    local sums = http.get({ url = dir .. "SHA256SUMS" })
    if sums.status_code ~= 200 then
        error("fetching SHA256SUMS failed: HTTP " .. sums.status_code)
    end
    local expected = acme.find_checksum(sums.body, filename)

    http.download_file({ url = dir .. filename }, archive)
    if acme.sha256(archive) ~= expected then
        error("checksum mismatch for " .. filename)
    end

    archiver.decompress(archive, ctx.install_path, { strip_components = 1 })
    return {}
end
hooks/backend_exec_env.lua
lua
local file = require("file")

function PLUGIN:BackendExecEnv(ctx)
    return {
        env_vars = {
            { key = "PATH", value = file.join_path(ctx.install_path, "bin") },
        },
    }
end

Configure a tool and its mirror, then install it:

mise.toml
toml
[tools]
"acme:deploy" = { version = "2.1.0", mirror = "https://artifacts.internal.example.com" }
sh
mise install
mise exec -- deploy --version

mise install calls BackendListVersions to resolve 2.1.0 and BackendInstall to download, verify and extract the archive into ~/.local/share/mise/installs/acme-deploy/2.1.0. BackendExecEnv then puts its bin directory on PATH. mise does not check what BackendInstall downloads, so the hook verifies the checksum itself before extracting.

Hooks ​

HookFileRequiredmise calls it
BackendListVersionshooks/backend_list_versions.luayesto list a tool's versions (mise ls-remote, resolving latest)
BackendInstallhooks/backend_install.luayesto install one version of a tool
BackendExecEnvhooks/backend_exec_env.luayesto build the environment for an installed version
BackendUninstallhooks/backend_uninstall.luanobefore mise removes an installed version
BackendListToolshooks/backend_list_tools.luanofor mise search, shell completion and the mise use picker
BackendSearchToolshooks/backend_search_tools.luanofor mise search and shell completion with a query

A hook fails by raising an error with error(); mise stops and shows the message. Hooks can use the http, json, file, archiver, cmd and other modules in the Plugin Lua reference, and the RUNTIME global that describes the platform.

BackendListVersions ​

ctx fieldValueExample
toolThe tool name after the prefix"deploy"
optionsThe tool's options; see Tool options{ mirror = "https://..." }

Return { versions = { ... } }, a list of version strings oldest first, according to the tool's release policy. mise keeps that order. Versions are not always SemVer: they can be dates, prereleases or channel names. A tool plugin's Available hook returns newest first instead.

BackendInstall ​

ctx fieldValueExample
toolThe tool name"deploy"
versionThe resolved version being installed"2.1.0"
install_pathThe directory to install into"/home/user/.local/share/mise/installs/acme-deploy/2.1.0"
download_pathA directory for downloads"/home/user/.local/share/mise/downloads/acme-deploy/2.1.0"
optionsThe tool's options; see Tool options{ mirror = "https://..." }

Install the version into install_path and return a table, even an empty one; returning nothing fails the install. http.download_file and archiver.decompress create the directories they write into.

mise does not verify what BackendInstall downloads, records no download URL or checksum for backend plugin tools in mise.lock, and does not support attestations here. Check a checksum before extracting, as the complete example does.

BackendExecEnv ​

ctx fieldValueExample
toolThe tool name"deploy"
versionThe installed version"2.1.0"
install_pathThe install directory"/home/user/.local/share/mise/installs/acme-deploy/2.1.0"
optionsThe tool's options; see Tool options{ mirror = "https://..." }

Return { env_vars = { { key = ..., value = ... }, ... } }. Implement this hook even when you have nothing to add, and return { env_vars = {} }. Return each PATH directory as its own entry; mise joins repeated keys with the platform's path separator. If you return no PATH entry, mise adds install_path's bin. mise caches the result for each version and set of options, so keep the hook fast.

BackendUninstall ​

ctx fieldValue
toolThe tool name
versionThe installed version
install_pathThe install directory, which still exists
download_pathThe download directory
optionsThe tool's options from the current config

Use this hook for cleanup that deleting the install directory cannot do, such as running a vendor uninstaller or removing entries created outside install_path:

hooks/backend_uninstall.lua
lua
function PLUGIN:BackendUninstall(ctx)
    -- undo changes made outside ctx.install_path; files inside it can still be read
end

mise calls this hook whenever it removes an installed version: mise uninstall, mise upgrade, mise prune, and delayed removal of old versions. It does not run for --dry-run. If the hook raises an error, mise stops and keeps the install directory so the uninstall can be retried.

ctx.options comes from the current config. When the tool is no longer configured, for example during mise prune, it contains only defaults. Save anything the uninstaller needs in install_path during BackendInstall instead of relying on ctx.options.

BackendListTools ​

BackendListTools receives an empty ctx, because no tool is selected yet. Return the tools the backend manages:

hooks/backend_list_tools.lua
lua
function PLUGIN:BackendListTools(ctx)
    return {
        tools = {
            { name = "deploy", description = "Deploy services" },
            { name = "lint", description = "Check configuration files" },
        },
    }
end

name is required and description is optional. mise prefixes each name with the plugin's install name and shows the resulting acme:deploy identifiers in mise search, shell completion and the interactive mise use picker. Return a short, finite catalog; a package-manager backend should not list a whole ecosystem. mise caches the response for the fetch_remote_versions_cache duration and uses the stale result when a refresh fails.

BackendSearchTools ​

ctx fieldValueExample
queryThe text to search for, after any prefix"dep"

Return the same shape as BackendListTools. mise calls this hook when mise search or shell completion has a non-empty query, so a backend can search a large or changing catalog without listing all of it. mise search acme:dep sends dep. A plugin can implement this hook, BackendListTools, or both, for example a short list of featured tools plus a registry search. mise caches each query's response separately.

Tool options ​

ctx.options holds the options on a tool's entry in mise.toml, such as "acme:deploy" = { version = "2.1.0", verify = false }, in every hook except BackendListTools and BackendSearchTools. Backend plugins receive options as tool plugins do, so verify = false arrives as the string "false", which is truthy in Lua; see tool options for the conversion rules, the options mise keeps to itself and the MISE_TOOL_OPTS__ environment variables.

metadata.lua ​

metadata.lua sets the global PLUGIN table with at least name and version; the Lua reference lists every field. A backend plugin that needs another tool while it installs, such as node for an npm-based backend, lists it in depends:

metadata.lua
lua
PLUGIN = {
    name = "my-npm",
    version = "0.1.0",
    depends = { "node" },
}

If the user configured node, mise installs it before this plugin's tools and puts it on PATH for the commands the hooks run; see depends.

Testing ​

sh
mise plugins link acme ./mise-acme
mise ls-remote acme:deploy
mise use acme:deploy@2.1.0
mise exec -- deploy --version

Run a failing command with debug output to see the hook's error and output, and clear a tool's cache when a cached version list or environment hides an edit to a hook:

sh
MISE_DEBUG=1 mise install acme:deploy@2.1.0
mise cache clear acme:deploy

Test on every platform you support, including a path that contains spaces. The template's mise run test task runs the same checks. See Publishing plugins for an isolated test setup.

Common mistakes ​

  • Returning two values from a helper. gsub returns the new string and a count, so return s:gsub("^v", "") makes table.insert(versions, normalize(tag)) fail with bad argument #2 to 'insert'. Wrap the call in parentheses:

    lua
    local function normalize(tag)
        return (tag:gsub("^v", "")) -- the parentheses drop gsub's count
    end
  • Stripping more than a prefix the publisher documents. Treat the rest of a version as opaque, and do not sort versions with a SemVer parser.

  • Searching cmd.exec output for the word error. cmd.exec raises an error with the command's stderr when it exits with a nonzero status.

  • Building shell commands from tool names, versions or options. Pass values in cmd.exec's env option and quote them, as the complete example does.

  • Shelling out to mv, mkdir or cp. Use file.move instead of mv. file.move, http.download_file and archiver.decompress create the directories they write into, so most plugins never need mkdir. The file module has no copy or mkdir function; if you shell out for those, quote every path and remember that the command differs on Windows.

  • Joining paths with .. and "/". Use file.join_path.

  • Expecting a Lua table to cache data between commands. Each mise command starts a new Lua runtime; mise caches version lists and environments itself. See cache behavior and the Lua reference.

To publish the plugin, see Publishing plugins. For another real backend plugin, see vfox-npm, which installs npm packages with the npm on PATH.

MIT LicenseCopyright © 2026jdx.dev